esky/fstransact/fallback.py
# Copyright (c) 2009-2010, Cloud Matrix Pty. Ltd.
# All rights reserved; available under the terms of the BSD License.
"""
esky.fstransact.fallback: fallback implementation for FSTransaction
"""
import os
import sys
import shutil
from esky.util import get_backup_filename, files_differ, really_rename
class FSTransaction(object):
"""Utility class for transactionally operating on the filesystem.
This particular implementation is the fallback for systems that don't
support transactional filesystem operations.
"""
def __init__(self,root=None):
if root is None:
self.root = None
else:
self.root = os.path.normpath(os.path.abspath(root))
if self.root.endswith(os.sep):
self.root = self.root[:-1]
self.pending = []
def _check_path(self,path):
if self.root is not None:
path = os.path.normpath(os.path.join(self.root,path))
if len(self.root) == 2 and sys.platform == "win32":
prefix = self.root
else:
prefix = self.root + os.sep
if not path.startswith(prefix):
err = "path is outside transaction root: %s" % (path,)
raise ValueError(err)
return path
def move(self,source,target):
source = self._check_path(source)
target = self._check_path(target)
if os.path.isdir(source):
if os.path.isdir(target):
s_names = os.listdir(source)
for nm in s_names:
self.move(os.path.join(source,nm),
os.path.join(target,nm))
for nm in os.listdir(target):
if nm not in s_names:
self.remove(os.path.join(target,nm))
self.remove(source)
else:
self.pending.append(("_move",source,target))
else:
if os.path.isdir(target) or files_differ(source,target):
self.pending.append(("_move",source,target))
else:
self.pending.append(("_remove",source))
def _move(self,source,target):
if sys.platform == "win32" and os.path.exists(target):
# os.rename won't overwite an existing file on win32.
# We also want to use this on files that are potentially open.
# Renaming the target out of the way is the best we can do :-(
target_old = target + ".old"
while os.path.exists(target_old):
target_old = target_old + ".old"
really_rename(target,target_old)
try:
really_rename(source,target)
except:
really_rename(target_old,target)
raise
else:
try:
self._remove(target_old)
except EnvironmentError:
pass
else:
target_old = None
if os.path.isdir(target) and os.path.isfile(source):
target_old = target + ".old"
while os.path.exists(target_old):
target_old = target_old + ".old"
really_rename(target,target_old)
elif os.path.isfile(target) and os.path.isdir(source):
target_old = target + ".old"
while os.path.exists(target_old):
target_old = target_old + ".old"
really_rename(target,target_old)
self._create_parents(target)
really_rename(source,target)
if target_old is not None:
self._remove(target_old)
def _create_parents(self,target):
parents = [target]
while not os.path.exists(os.path.dirname(parents[-1])):
parents.append(os.path.dirname(parents[-1]))
for parent in reversed(parents[1:]):
os.mkdir(parent)
def copy(self,source,target):
source = self._check_path(source)
target = self._check_path(target)
if os.path.isdir(source):
if os.path.isdir(target):
s_names = os.listdir(source)
for nm in s_names:
self.copy(os.path.join(source,nm),
os.path.join(target,nm))
for nm in os.listdir(target):
if nm not in s_names:
self.remove(os.path.join(target,nm))
else:
self.pending.append(("_copy",source,target))
else:
if os.path.isdir(target) or files_differ(source,target):
self.pending.append(("_copy",source,target))
def _copy(self,source,target):
is_win32 = (sys.platform == "win32")
if is_win32 and os.path.exists(target) and target != source:
target_old = get_backup_filename(target)
really_rename(target,target_old)
try:
self._do_copy(source,target)
except:
really_rename(target_old,target)
raise
else:
try:
os.unlink(target_old)
except EnvironmentError:
pass
else:
target_old = None
if os.path.isdir(target) and os.path.isfile(source):
target_old = get_backup_filename(target)
really_rename(target,target_old)
elif os.path.isfile(target) and os.path.isdir(source):
target_old = get_backup_filename(target)
really_rename(target,target_old)
self._do_copy(source,target)
if target_old is not None:
self._remove(target_old)
def _do_copy(self,source,target):
self._create_parents(target)
if os.path.isfile(source):
shutil.copy2(source,target)
else:
shutil.copytree(source,target)
def remove(self,target):
target = self._check_path(target)
self.pending.append(("_remove",target))
def _remove(self,target):
if os.path.isfile(target):
os.unlink(target)
elif os.path.isdir(target):
for nm in os.listdir(target):
self._remove(os.path.join(target,nm))
os.rmdir(target)
def commit(self):
for op in self.pending:
getattr(self,op[0])(*op[1:])
def abort(self):
del self.pending[:]