0

Now writes images to temporary files and atomically moves in place*

* on systems with an atomic os.rename
This commit is contained in:
Andrew Brown
2012-02-26 00:14:58 -05:00
parent c759b20f8e
commit 19f6f136e4
3 changed files with 102 additions and 40 deletions

View File

@@ -144,11 +144,10 @@ directory.
jsondump = json.dumps(dump, indent=4) jsondump = json.dumps(dump, indent=4)
with codecs.open(os.path.join(self.outputdir, 'overviewerConfig.js'), 'w', encoding='UTF-8') as f: with util.FileReplacer(os.path.join(self.outputdir, "overviewerConfig.js")) as tmpfile:
f.write("var overviewerConfig = " + jsondump + ";\n") with codecs.open(tmpfile, 'w', encoding='UTF-8') as f:
f.write("var overviewerConfig = " + jsondump + ";\n")
# copy web assets into destdir: # copy web assets into destdir:
global_assets = os.path.join(util.get_program_path(), "overviewer_core", "data", "web_assets") global_assets = os.path.join(util.get_program_path(), "overviewer_core", "data", "web_assets")
if not os.path.isdir(global_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") js_src = os.path.join(util.get_program_path(), "overviewer_core", "data", "js_src")
if not os.path.isdir(js_src): if not os.path.isdir(js_src):
js_src = os.path.join(util.get_program_path(), "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: with util.FileReplacer(os.path.join(self.outputdir, "overviewer.js")) as tmpfile:
# first copy in js_src/overviewer.js with open(tmpfile, "w") as fout:
with open(os.path.join(js_src, "overviewer.js")) as f: # first copy in js_src/overviewer.js
fout.write(f.read()) with open(os.path.join(js_src, "overviewer.js"), 'r') as f:
# now copy in the rest fout.write(f.read())
for js in os.listdir(js_src): # now copy in the rest
if not js.endswith("overviewer.js") and js.endswith(".js"): for js in os.listdir(js_src):
with open(os.path.join(js_src,js)) as f: if not js.endswith("overviewer.js") and js.endswith(".js"):
fout.write(f.read()) 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)
# helper function to get a label for the given rendermode # helper function to get a label for the given rendermode
def get_render_mode_label(rendermode): def get_render_mode_label(rendermode):
info = get_render_mode_info(rendermode) info = get_render_mode_info(rendermode)
@@ -193,7 +185,6 @@ directory.
versionstr = "%s (%s)" % (overviewer_version.VERSION, overviewer_version.HASH[:7]) versionstr = "%s (%s)" % (overviewer_version.VERSION, overviewer_version.HASH[:7])
index = index.replace("{version}", versionstr) index = index.replace("{version}", versionstr)
with codecs.open(os.path.join(self.outputdir, "index.html"), 'w', encoding='UTF-8') as output: with util.FileReplacer(indexpath) as indexpath:
output.write(index) with codecs.open(indexpath, 'w', encoding='UTF-8') as output:
output.write(index)

View File

@@ -28,6 +28,7 @@ from collections import namedtuple
from PIL import Image from PIL import Image
from .util import iterate_base4, convert_coords, unconvert_coords, get_tiles_by_chunk from .util import iterate_base4, convert_coords, unconvert_coords, get_tiles_by_chunk
from .util import FileReplacer
from .optimizeimages import optimize_image from .optimizeimages import optimize_image
import c_overviewer 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) 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 # Save it
if imgformat == 'jpg': with FileReplacer(imgpath) as tmppath:
img.save(imgpath, quality=self.options['imgquality'], subsampling=0) if imgformat == 'jpg':
else: # png img.save(tmppath, "jpeg", quality=self.options['imgquality'], subsampling=0)
img.save(imgpath) else: # png
img.save(tmppath, "png")
if self.options['optimizeimg']:
optimize_image(imgpath, imgformat, self.options['optimizeimg']) 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): def _render_rendertile(self, tile):
"""Renders the given render-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') draw.text((96,96), "c,r: %s,%s" % (col, row), fill='red')
# Save them # Save them
if self.imgextension == 'jpg': with FileReplacer(imgpath) as tmppath:
tileimg.save(imgpath, quality=self.options['imgquality'], subsampling=0) if self.imgextension == 'jpg':
else: # png tileimg.save(tmppath, "jpeg", quality=self.options['imgquality'], subsampling=0)
tileimg.save(imgpath) else: # png
tileimg.save(tmppath, "png")
if self.options['optimizeimg']: if self.options['optimizeimg']:
optimize_image(imgpath, self.imgextension, 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): def _get_chunks_for_tile(self, tile):
"""Get chunks that are relevant to the given render-tile """Get chunks that are relevant to the given render-tile

View File

@@ -178,6 +178,74 @@ def get_tiles_by_chunk(chunkcol, chunkrow):
return product(colrange, rowrange) 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 # Logging related classes are below
# Some cool code for colored logging: # Some cool code for colored logging: