diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/survival/PhotoService.java b/src/main/java/eu/mhsl/minecraft/pixelpics/survival/PhotoService.java index 635bce0..27a4311 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/survival/PhotoService.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/survival/PhotoService.java @@ -53,6 +53,13 @@ public final class PhotoService { return false; } + if (!MapManager.canStore()) { + player.sendActionBar(Component.text( + "Fotos können gerade nicht gespeichert werden — bitte einem Admin melden.", NamedTextColor.RED)); + plugin.getLogger().warning("Photo of " + player.getName() + " refused: the image folder is not writable."); + return false; + } + RenderManager manager = plugin.getRenderManager(); UUID user = player.getUniqueId(); RenderManager.Outcome outcome = manager.tryReserve(user); @@ -118,7 +125,12 @@ public final class PhotoService { }, out -> { chunks.release(); // already released by prepare - MapManager.saveImage(out.image(), id); + if (!MapManager.saveImage(out.image(), id)) { + failed( + id, user, + usesFilm); // not stored: it would be lost (or a vanilla map) after a restart + return; + } MapManager.saveIndices(out.indices(), id); plugin.getPendingPhotos().remove(id); mapRenderer.develop(out.indices()); diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/utils/MapManager.java b/src/main/java/eu/mhsl/minecraft/pixelpics/utils/MapManager.java index 078443a..186248f 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/utils/MapManager.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/utils/MapManager.java @@ -2,9 +2,12 @@ package eu.mhsl.minecraft.pixelpics.utils; import eu.mhsl.minecraft.pixelpics.Main; import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; +import java.nio.file.AtomicMoveNotSupportedException; import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.Arrays; @@ -173,28 +176,58 @@ public class MapManager { + backup + "."); } - /** Blank film indices for a photo that has not developed (yet): never a vanilla world map after a restart. */ - public static void savePlaceholder(int id) { - byte[] blank = new byte[INDEX_BYTES]; - Arrays.fill(blank, ImageMapRenderer.filmColor()); - saveIndices(blank, id); + /** Whether new photos can be stored: the folder exists (or was created) and is writable. */ + public static boolean canStore() { + return ensureFolder() && Files.isWritable(imageFolder().toPath()); } - public static void saveImage(BufferedImage image, int id) { - if (!ensureFolder()) return; + /** + * Blank film indices for a photo that has not developed (yet): never a vanilla world map after a restart. False if + * they could not be stored. + */ + public static boolean savePlaceholder(int id) { + byte[] blank = new byte[INDEX_BYTES]; + Arrays.fill(blank, ImageMapRenderer.filmColor()); + return saveIndices(blank, id); + } + + /** Stores the PNG (atomically: a crash never leaves a truncated one); false if it could not be written. */ + public static boolean saveImage(BufferedImage image, int id) { + if (!ensureFolder()) return false; try { - ImageIO.write(image, "png", getImagePath(id)); + ByteArrayOutputStream png = new ByteArrayOutputStream(); + if (!ImageIO.write(image, "png", png)) throw new IOException("no PNG writer"); + writeAtomically(getImagePath(id), png.toByteArray()); + return true; } catch (IOException e) { - Main.getInstance().getLogger().warning("Failed to save map image " + id + ": " + e.getMessage()); + Main.getInstance().getLogger().warning("Failed to save map image " + id + ": " + e); + return false; } } - public static void saveIndices(byte[] indices, int id) { - if (indices == null || indices.length != INDEX_BYTES || !ensureFolder()) return; + /** Stores the index cache (atomically); false if it could not be written. */ + public static boolean saveIndices(byte[] indices, int id) { + if (indices == null || indices.length != INDEX_BYTES || !ensureFolder()) return false; try { - Files.write(getIndexPath(id).toPath(), indices); + writeAtomically(getIndexPath(id), indices); + return true; } catch (IOException e) { - Main.getInstance().getLogger().warning("Failed to save map index cache " + id + ": " + e.getMessage()); + Main.getInstance().getLogger().warning("Failed to save map index cache " + id + ": " + e); + return false; + } + } + + private static void writeAtomically(File target, byte[] data) throws IOException { + Path tmp = target.toPath().resolveSibling(target.getName() + ".tmp"); + try { + Files.write(tmp, data); + try { + Files.move(tmp, target.toPath(), StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + } catch (AtomicMoveNotSupportedException e) { + Files.move(tmp, target.toPath(), StandardCopyOption.REPLACE_EXISTING); + } + } finally { + Files.deleteIfExists(tmp); } } @@ -215,8 +248,10 @@ public class MapManager { File path = getImagePath(id); if (!path.isFile()) return null; try { - return ImageIO.read(path); - } catch (IOException e) { + BufferedImage image = ImageIO.read(path); + if (image == null) Main.getInstance().getLogger().warning("Map image " + id + " is no readable image."); + return image; + } catch (IOException | RuntimeException e) { Main.getInstance().getLogger().warning("Failed to read map image " + id + ": " + e.getMessage()); return null; }