# This file is part of the Minecraft Overviewer. # # Minecraft Overviewer is free software: you can redistribute it and/or # modify it under the terms of the GNU General Public License as published # by the Free Software Foundation, either version 3 of the License, or (at # your option) any later version. # # Minecraft Overviewer is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General # Public License for more details. # # You should have received a copy of the GNU General Public License along # with the Overviewer. If not, see . import os import os.path import tempfile import shutil import logging import stat LOG = logging.getLogger(__name__) ## useful recursive copy, that ignores common OS cruft def mirror_dir(src, dst, entities=None): '''copies all of the entities from src to dst''' if not os.path.exists(dst): os.mkdir(dst) if entities and type(entities) != list: raise Exception("Expected a list, got a %r instead" % type(entities)) # files which are problematic and should not be copied # usually, generated by the OS skip_files = ['Thumbs.db', '.DS_Store'] for entry in os.listdir(src): if entry in skip_files: continue if entities and entry not in entities: continue if os.path.isdir(os.path.join(src,entry)): mirror_dir(os.path.join(src, entry), os.path.join(dst, entry)) elif os.path.isfile(os.path.join(src,entry)): try: shutil.copy(os.path.join(src, entry), os.path.join(dst, entry)) except IOError as outer: try: # maybe permission problems? src_stat = os.stat(os.path.join(src, entry)) os.chmod(os.path.join(src, entry), src_stat.st_mode | stat.S_IRUSR) dst_stat = os.stat(os.path.join(dst, entry)) os.chmod(os.path.join(dst, entry), dst_stat.st_mode | stat.S_IWUSR) except OSError: # we don't care if this fails pass shutil.copy(os.path.join(src, entry), os.path.join(dst, entry)) # if this stills throws an error, let it propagate up # Define a context manager to handle atomic renaming or "just forget it write # straight to the file" depending on whether os.rename provides atomic # overwrites. # Detect whether os.rename will overwrite files with tempfile.NamedTemporaryFile() as f1: with tempfile.NamedTemporaryFile() as f2: try: os.rename(f1.name,f2.name) except OSError: renameworks = False else: renameworks = True # re-make this file so it can be deleted without error open(f1.name, 'w').close() del tempfile,f1,f2 doc = """This class acts as a context manager for files that are to be written out overwriting an existing file. The parameter is the destination filename. The value returned into the context is the filename that should be used. On systems that support an atomic os.rename(), the filename will actually be a temporary file, and it will be atomically replaced over the destination file on exit. On systems that don't support an atomic rename, the filename returned is the filename given. If an error is encountered, the file is attempted to be removed, and the error is propagated. Example: with FileReplacer("config") as configname: with open(configout, 'w') as configout: configout.write(newconfig) """ if renameworks: class FileReplacer(object): __doc__ = doc def __init__(self, destname): self.destname = destname self.tmpname = destname + ".tmp" def __enter__(self): # rename works here. Return a temporary filename return self.tmpname def __exit__(self, exc_type, exc_val, exc_tb): if exc_type: # error try: os.remove(self.tmpname) except Exception, e: LOG.warning("An error was raised, so I was doing " "some cleanup first, but I couldn't remove " "'%s'!", self.tmpname) else: # copy permission bits, if needed if os.path.exists(self.destname): shutil.copymode(self.destname, self.tmpname) # atomic rename into place os.rename(self.tmpname, self.destname) else: class FileReplacer(object): __doc__ = doc def __init__(self, destname): self.destname = destname def __enter__(self): return self.destname def __exit__(self, exc_type, exc_val, exc_tb): return del renameworks