From 612a8e1183e6c6f3e973ada6f449a9c5df5a9f42 Mon Sep 17 00:00:00 2001 From: Jeremy Fincher Date: Tue, 5 Jul 2005 17:48:00 +0000 Subject: [PATCH] Added utils.{transaction,error} --- src/utils/__init__.py | 2 + src/utils/error.py | 45 ++++++++++ src/utils/file.py | 26 ++++++ src/utils/transaction.py | 179 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 252 insertions(+) create mode 100644 src/utils/error.py create mode 100644 src/utils/transaction.py diff --git a/src/utils/__init__.py b/src/utils/__init__.py index b18702384..57d1e7baa 100644 --- a/src/utils/__init__.py +++ b/src/utils/__init__.py @@ -102,6 +102,8 @@ import seq import str import file import iter +import error import python +import transaction # vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: diff --git a/src/utils/error.py b/src/utils/error.py new file mode 100644 index 000000000..c461e50a8 --- /dev/null +++ b/src/utils/error.py @@ -0,0 +1,45 @@ +### +# Copyright (c) 2005, Jeremiah Fincher +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions, and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions, and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of the author of this software nor the name of +# contributors to this software may be used to endorse or promote products +# derived from this software without specific prior written consent. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +### + +import os + +import gen + +class Error(Exception): + def __init__(self, msg, e=None): + self.msg = msg + self.e = e + + def __str__(self): + if self.e is not None: + return os.linesep.join([self.msg, gen.exnToString(self.e)]) + else: + return self.msg + +# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78: diff --git a/src/utils/file.py b/src/utils/file.py index c3237df08..00507616c 100644 --- a/src/utils/file.py +++ b/src/utils/file.py @@ -36,6 +36,32 @@ import shutil import os.path from iter import ifilter +def contents(filename): + return file(filename).read() + +def open(filename, mode='wb', *args, **kwargs): + """filename -> file object. + + Returns a file object for filename, creating as many directories as may be + necessary. I.e., if the filename is ./foo/bar/baz, and . exists, and ./foo + exists, but ./foo/bar does not exist, bar will be created before opening + baz in it. + """ + if mode not in ('w', 'wb'): + raise ValueError, 'utils.file.open expects to write.' + (dirname, basename) = os.path.split(filename) + os.makedirs(dirname) + return file(filename, mode, *args, **kwargs) + +def copy(src, dst): + """src, dst -> None + + Copies src to dst, using this module's 'open' function to open dst. + """ + srcfd = file(src) + dstfd = open(dst, 'wb') + shutil.copyfileobj(srcfd, dstfd) + def writeLine(fd, line): fd.write(line) if not line.endswith('\n'): diff --git a/src/utils/transaction.py b/src/utils/transaction.py new file mode 100644 index 000000000..42953780c --- /dev/null +++ b/src/utils/transaction.py @@ -0,0 +1,179 @@ +### +# Copyright (c) 2005, Jeremiah Fincher +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions, and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions, and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of the author of this software nor the name of +# contributors to this software may be used to endorse or promote products +# derived from this software without specific prior written consent. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +### + +""" +Defines a Transaction class for multi-file transactions. +""" + +import os +import shutil +import os.path + +import error +import python +import file as File + +# 'txn' is used as an abbreviation for 'transaction' in the following source. + +class FailedAcquisition(error.Error): + def __init__(self, txnDir, e=None): + self.txnDir = txnDir + msg = 'Could not acquire transaction directory: %s.' % self.txnDir + error.Error.__init__(self, msg, e) + +class InProgress(error.Error): + def __init__(self, inProgress, e=None): + self.inProgress = inProgress + msg = 'Transaction appears to be in progress already: %s exists.' % \ + self.inProgress + error.Error.__init__(self, msg, e) + + +class TransactionMixin(python.Object): + JOURNAL = 'journal' + ORIGINALS = 'originals' + INPROGRESS = '.inProgress' + REPLACEMENTS = 'replacements' + # expects a self.dir. used by Transaction and Rollback. + def __init__(self, txnDir): + self.txnDir = txnDir + self.dir = self.txnDir + self.INPROGRESS + self._journalName = self.dirize(self.JOURNAL) + + def escape(self, filename): + return os.path.abspath(filename)[1:] + + def dirize(self, *args): + return os.path.join(self.dir, *args) + + def _original(self, filename): + return self.dirize(self.ORIGINALS, self.escape(filename)) + + def _replacement(self, filename): + return self.dirize(self.REPLACEMENTS, self.escape(filename)) + + def checkCwd(self): + expected = File.contents(self.dirize('cwd')) + if os.getcwd() != expected: + raise InvalidCwd(expected) + + def journalCommands(self): + journal = file(self._journalName) + for line in journal: + line = line.rstrip('\n') + (command, rest) = line.split(None, 1) + args = rest.split() + yield (command, args) + + +class Transaction(TransactionMixin): + def __init__(self, *args, **kwargs): + """Transaction(root, txnDir) -> None + + txnDir is the directory that will hold the transaction's working files + and such. If it can't be renamed, there is probably an active + transaction. + """ + TransactionMixin.__init__(self, *args, **kwargs) + if os.path.exists(self.dir): + raise InProgress(self.dir) + if not os.path.exists(self.txnDir): + raise FailedAcquisition(self.txnDir) + try: + os.rename(self.txnDir, self.dir) + except EnvironmentError, e: + raise TransactionAcquisitionFailure(self.txnDir, e) + os.mkdir(self.dirize(self.ORIGINALS)) + os.mkdir(self.dirize(self.REPLACEMENTS)) + self._journal = file(self._journalName, 'a') + cwd = file(self.dirize('cwd'), 'w') + cwd.write(os.getcwd()) + cwd.close() + + def journalCommand(self, command, *args): + File.writeLine(self._journal, + '%s %s' % (command, ' '.join(map(str, args)))) + self._journal.flush() + + def _makeOriginal(self, filename): + File.copy(filename, self._original(filename)) + + def replace(self, filename): + self.checkCwd() + self._makeOriginal(filename) + self.journalCommand('replace', filename) + return File.open(self._replacement(filename)) + + def append(self, filename): + self.checkCwd() + length = os.stat(filename).st_size + self.journalCommand('append', filename, length) + replacement = self._replacement(filename) + File.copy(filename, replacement) + return file(replacement, 'a') + + def commit(self, removeWhenComplete=True): + self._journal.close() + self.checkCwd() + File.touch(self.dirize('commit')) + for (command, args) in self.journalCommands(): + methodName = 'commit%s' % command.capitalize() + getattr(self, methodName)(*args) + File.touch(self.dirize('committed')) + if removeWhenComplete: + shutil.rmtree(self.dir) + + def commitReplace(self, filename): + shutil.copy(self._replacement(filename), filename) + + def commitAppend(self, filename, length): + shutil.copy(self._replacement(filename), filename) + + +class Rollback(TransactionMixin): + def rollback(self, removeWhenComplete=True): + self.checkCwd() + if not os.path.exists(self.dirize('commit')): + return # No action taken; commit hadn't begun. + for (command, args) in self.journalCommands(): + methodName = 'rollback%s' % command.capitalize() + getattr(self, methodName)(*args) + if removeWhenComplete: + shutil.rmtree(self.dir) + + def rollbackReplace(self, filename): + shutil.copy(self._original(filename), filename) + + def rollbackAppend(self, filename, length): + fd = file(filename, 'a') + fd.truncate(int(length)) + fd.close() + + +# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78: