diff --git a/overviewer_core/assetmanager.py b/overviewer_core/assetmanager.py index 97d9ab0..26734b4 100644 --- a/overviewer_core/assetmanager.py +++ b/overviewer_core/assetmanager.py @@ -144,11 +144,10 @@ directory. jsondump = json.dumps(dump, indent=4) - with codecs.open(os.path.join(self.outputdir, 'overviewerConfig.js'), 'w', encoding='UTF-8') as f: - f.write("var overviewerConfig = " + jsondump + ";\n") + with util.FileReplacer(os.path.join(self.outputdir, "overviewerConfig.js")) as tmpfile: + with codecs.open(tmpfile, 'w', encoding='UTF-8') as f: + f.write("var overviewerConfig = " + jsondump + ";\n") - - # copy web assets into destdir: global_assets = os.path.join(util.get_program_path(), "overviewer_core", "data", "web_assets") if not os.path.isdir(global_assets): @@ -159,23 +158,16 @@ directory. js_src = os.path.join(util.get_program_path(), "overviewer_core", "data", "js_src") if not os.path.isdir(js_src): js_src = os.path.join(util.get_program_path(), "js_src") - with open(os.path.join(self.outputdir, "overviewer.js"), "w") as fout: - # first copy in js_src/overviewer.js - with open(os.path.join(js_src, "overviewer.js")) as f: - fout.write(f.read()) - # now copy in the rest - for js in os.listdir(js_src): - if not js.endswith("overviewer.js") and js.endswith(".js"): - with open(os.path.join(js_src,js)) as f: - fout.write(f.read()) - - # do the same with the local copy, if we have it - # TODO - # if self.web_assets_path: - # util.mirror_dir(self.web_assets_path, self.outputdir) - - - + with util.FileReplacer(os.path.join(self.outputdir, "overviewer.js")) as tmpfile: + with open(tmpfile, "w") as fout: + # first copy in js_src/overviewer.js + with open(os.path.join(js_src, "overviewer.js"), 'r') as f: + fout.write(f.read()) + # now copy in the rest + for js in os.listdir(js_src): + if not js.endswith("overviewer.js") and js.endswith(".js"): + with open(os.path.join(js_src,js)) as f: + fout.write(f.read()) # helper function to get a label for the given rendermode def get_render_mode_label(rendermode): info = get_render_mode_info(rendermode) @@ -193,7 +185,6 @@ directory. versionstr = "%s (%s)" % (overviewer_version.VERSION, overviewer_version.HASH[:7]) index = index.replace("{version}", versionstr) - with codecs.open(os.path.join(self.outputdir, "index.html"), 'w', encoding='UTF-8') as output: - output.write(index) - - + with util.FileReplacer(indexpath) as indexpath: + with codecs.open(indexpath, 'w', encoding='UTF-8') as output: + output.write(index) diff --git a/overviewer_core/tileset.py b/overviewer_core/tileset.py index 74d2684..e815907 100644 --- a/overviewer_core/tileset.py +++ b/overviewer_core/tileset.py @@ -28,6 +28,7 @@ from collections import namedtuple from PIL import Image from .util import iterate_base4, convert_coords, unconvert_coords, get_tiles_by_chunk +from .util import FileReplacer from .optimizeimages import optimize_image import c_overviewer @@ -784,15 +785,16 @@ class TileSet(object): logging.error("While attempting to delete corrupt image %s, an error was encountered. You will need to delete it yourself. Error was '%s'", path[1], e) # Save it - if imgformat == 'jpg': - img.save(imgpath, quality=self.options['imgquality'], subsampling=0) - else: # png - img.save(imgpath) - - if self.options['optimizeimg']: - optimize_image(imgpath, imgformat, self.options['optimizeimg']) + with FileReplacer(imgpath) as tmppath: + if imgformat == 'jpg': + img.save(tmppath, "jpeg", quality=self.options['imgquality'], subsampling=0) + else: # png + img.save(tmppath, "png") + + if self.options['optimizeimg']: + optimize_image(tmppath, imgformat, self.options['optimizeimg']) - os.utime(imgpath, (max_mtime, max_mtime)) + os.utime(tmppath, (max_mtime, max_mtime)) def _render_rendertile(self, tile): """Renders the given render-tile. @@ -877,15 +879,16 @@ class TileSet(object): draw.text((96,96), "c,r: %s,%s" % (col, row), fill='red') # Save them - if self.imgextension == 'jpg': - tileimg.save(imgpath, quality=self.options['imgquality'], subsampling=0) - else: # png - tileimg.save(imgpath) + with FileReplacer(imgpath) as tmppath: + if self.imgextension == 'jpg': + tileimg.save(tmppath, "jpeg", quality=self.options['imgquality'], subsampling=0) + else: # png + tileimg.save(tmppath, "png") - if self.options['optimizeimg']: - optimize_image(imgpath, self.imgextension, self.options['optimizeimg']) + if self.options['optimizeimg']: + optimize_image(tmppath, self.imgextension, self.options['optimizeimg']) - os.utime(imgpath, (max_chunk_mtime, max_chunk_mtime)) + os.utime(tmppath, (max_chunk_mtime, max_chunk_mtime)) def _get_chunks_for_tile(self, tile): """Get chunks that are relevant to the given render-tile diff --git a/overviewer_core/util.py b/overviewer_core/util.py index 10db859..0c1cfbc 100644 --- a/overviewer_core/util.py +++ b/overviewer_core/util.py @@ -178,6 +178,74 @@ def get_tiles_by_chunk(chunkcol, chunkrow): return product(colrange, rowrange) +# 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 +import tempfile +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: + logging.warning("An error was raised, so I was doing " + "some cleanup first, but I couldn't remove " + "'%s'!", self.tmpname) + else: + # 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 + # Logging related classes are below # Some cool code for colored logging: