mirror of
https://github.com/Mikaela/Limnoria.git
synced 2024-12-30 23:02:44 +01:00
228 lines
8.6 KiB
Python
228 lines
8.6 KiB
Python
###
|
|
# 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
|
|
|
|
from . import error, file as File, python
|
|
|
|
# '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 InvalidCwd(Exception):
|
|
pass
|
|
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 = open(self._journalName)
|
|
for line in journal:
|
|
line = line.rstrip('\n')
|
|
(command, rest) = line.split(None, 1)
|
|
args = rest.split()
|
|
yield (command, args)
|
|
journal.close()
|
|
|
|
|
|
class Transaction(TransactionMixin):
|
|
# XXX Transaction needs to be made threadsafe.
|
|
def __init__(self, *args, **kwargs):
|
|
"""Transaction(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 as e:
|
|
raise FailedAcquisition(self.txnDir, e)
|
|
os.mkdir(self.dirize(self.ORIGINALS))
|
|
os.mkdir(self.dirize(self.REPLACEMENTS))
|
|
self._journal = open(self._journalName, 'a')
|
|
cwd = open(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))
|
|
|
|
# XXX There needs to be a way, given a transaction, to get a
|
|
# "sub-transaction", which:
|
|
#
|
|
# 1. Doesn't try to grab the txnDir and move it, but instead is just
|
|
# given the actual directory being used and uses that.
|
|
# 2. Acquires the lock of the original transaction, only releasing it
|
|
# when its .commit method is called (assuming Transaction is
|
|
# threadsafe).
|
|
# 3. Has a no-op .commit method (i.e., doesn't commit).
|
|
#
|
|
# This is so that, for instance, an object with an active Transaction
|
|
# can give other objects a Transaction-ish object without worrying that
|
|
# the transaction will be committed, while still allowing those objects
|
|
# to work properly with real transactions (i.e., they still call
|
|
# as they would on a normal Transaction, it just has no effect with a
|
|
# sub-transaction).
|
|
# The method that returns a subtransaction should be called "child."
|
|
def child(self):
|
|
raise NotImplementedError
|
|
|
|
# XXX create, replace, etc. return file objects. This class should keep a
|
|
# list of such file descriptors and only allow a commit if all of them
|
|
# are closed. Trying to commit with open file objects should raise an
|
|
# exception.
|
|
def create(self, filename):
|
|
"""
|
|
Returns a file object for a filename that should be created (with
|
|
the contents as they were written to the filename) when the transaction
|
|
is committed.
|
|
"""
|
|
raise NotImplementedError # XXX.
|
|
|
|
def mkdir(self, filename):
|
|
raise NotImplementedError # XXX
|
|
|
|
def delete(self, filename):
|
|
raise NotImplementedError # XXX
|
|
|
|
def replace(self, filename):
|
|
"""
|
|
Returns a file object for a filename that should be replaced by the
|
|
contents written to the file object when the transaction is committed.
|
|
"""
|
|
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 open(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)
|
|
|
|
# XXX need to be able to rename files transactionally. (hard; especially
|
|
# with renames that depend on one another. It might be easier to do
|
|
# rename separate from relocate.)
|
|
|
|
|
|
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 = open(filename, 'a')
|
|
fd.truncate(int(length))
|
|
fd.close()
|
|
|
|
|
|
# vim:set shiftwidth=4 softtabstop=8 expandtab textwidth=78:
|