text: #  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[:]
  
  
  