diff --git a/patches/net/minecraft/nbt/NbtIo.java.patch b/patches/net/minecraft/nbt/NbtIo.java.patch index 5c25d66383a..0dec753d04a 100644 --- a/patches/net/minecraft/nbt/NbtIo.java.patch +++ b/patches/net/minecraft/nbt/NbtIo.java.patch @@ -1,48 +1,5 @@ --- a/net/minecraft/nbt/NbtIo.java +++ b/net/minecraft/nbt/NbtIo.java -@@ -72,12 +_,19 @@ - } - - public static void writeCompressed(CompoundTag p_128945_, Path p_309705_) throws IOException { -+ // Neo: write to a temporary file and move it to the correct location instead -+ var tempFile = Files.createTempFile(p_309705_.getParent(), p_309705_.getFileName().toString(), "write-tmp"); - try ( -- OutputStream outputstream = Files.newOutputStream(p_309705_, SYNC_OUTPUT_OPTIONS); -+ OutputStream outputstream = Files.newOutputStream(tempFile, SYNC_OUTPUT_OPTIONS); - OutputStream outputstream1 = new BufferedOutputStream(outputstream); - ) { - writeCompressed(p_128945_, outputstream1); - } -+ try { -+ Files.move(tempFile, p_309705_, java.nio.file.StandardCopyOption.ATOMIC_MOVE); -+ } catch(java.nio.file.AtomicMoveNotSupportedException e) { -+ Files.move(tempFile, p_309705_, java.nio.file.StandardCopyOption.REPLACE_EXISTING); -+ } - } - - public static void writeCompressed(CompoundTag p_128948_, OutputStream p_128949_) throws IOException { -@@ -87,13 +_,20 @@ - } - - public static void write(CompoundTag p_128956_, Path p_309549_) throws IOException { -+ // Neo: write to a temporary file and move it to the correct location instead -+ var tempFile = Files.createTempFile(p_309549_.getParent(), p_309549_.getFileName().toString(), "write-tmp"); - try ( -- OutputStream outputstream = Files.newOutputStream(p_309549_, SYNC_OUTPUT_OPTIONS); -+ OutputStream outputstream = Files.newOutputStream(tempFile, SYNC_OUTPUT_OPTIONS); - OutputStream outputstream1 = new BufferedOutputStream(outputstream); - DataOutputStream dataoutputstream = new DataOutputStream(outputstream1); - ) { - write(p_128956_, dataoutputstream); - } -+ try { -+ Files.move(tempFile, p_309549_, java.nio.file.StandardCopyOption.ATOMIC_MOVE); -+ } catch(java.nio.file.AtomicMoveNotSupportedException e) { -+ Files.move(tempFile, p_309549_, java.nio.file.StandardCopyOption.REPLACE_EXISTING); -+ } - } - - @Nullable @@ -178,10 +_,12 @@ private static Tag readUnnamedTag(DataInput p_128931_, NbtAccounter p_128933_) throws IOException { diff --git a/patches/net/minecraft/world/level/saveddata/SavedData.java.patch b/patches/net/minecraft/world/level/saveddata/SavedData.java.patch index b8779d52d89..e0f5247cf88 100644 --- a/patches/net/minecraft/world/level/saveddata/SavedData.java.patch +++ b/patches/net/minecraft/world/level/saveddata/SavedData.java.patch @@ -1,5 +1,14 @@ --- a/net/minecraft/world/level/saveddata/SavedData.java +++ b/net/minecraft/world/level/saveddata/SavedData.java +@@ -37,7 +_,7 @@ + NbtUtils.addCurrentDataVersion(compoundtag); + + try { +- NbtIo.writeCompressed(compoundtag, p_77758_.toPath()); ++ net.neoforged.neoforge.common.IOUtilities.writeNbtCompressed(compoundtag, p_77758_.toPath()); + } catch (IOException ioexception) { + LOGGER.error("Could not save data {}", this, ioexception); + } @@ -47,7 +_,10 @@ } diff --git a/patches/net/minecraft/world/level/storage/DimensionDataStorage.java.patch b/patches/net/minecraft/world/level/storage/DimensionDataStorage.java.patch index a03bf660fc8..feb8a8fee43 100644 --- a/patches/net/minecraft/world/level/storage/DimensionDataStorage.java.patch +++ b/patches/net/minecraft/world/level/storage/DimensionDataStorage.java.patch @@ -31,7 +31,7 @@ File file1 = this.getDataFile(p_78159_); CompoundTag compoundtag1; -@@ -98,8 +_,25 @@ +@@ -98,9 +_,16 @@ } } @@ -43,19 +43,10 @@ + } else { + compoundtag1 = compoundtag; + } -+ } + } + + // Neo: delete any temporary files so that we don't inflate disk space unnecessarily. -+ try (var filesToDelete = java.nio.file.Files.find( -+ file1.toPath().getParent(), 1, -+ (file, attributes) -> -+ file.getFileName().toString().startsWith(file1.getName()) -+ && file.getFileName().toString().endsWith("write-tmp"))) { -+ -+ for (var file : filesToDelete.toList()) -+ { -+ java.nio.file.Files.deleteIfExists(file); -+ } - } ++ net.neoforged.neoforge.common.IOUtilities.cleanupTempFiles(this.dataFolder.toPath(), p_78159_); return compoundtag1; + } diff --git a/src/main/java/net/neoforged/neoforge/common/IOUtilities.java b/src/main/java/net/neoforged/neoforge/common/IOUtilities.java new file mode 100644 index 00000000000..e6b1a5ea50b --- /dev/null +++ b/src/main/java/net/neoforged/neoforge/common/IOUtilities.java @@ -0,0 +1,160 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.common; + +import java.io.BufferedOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.function.BiPredicate; +import java.util.function.Consumer; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtIo; +import org.jetbrains.annotations.Nullable; + +/** + * Declares a class containing helpers for performing file I/O in a resillient + * manner. + */ +public final class IOUtilities { + private static final String TEMP_FILE_SUFFIX = ".neoforge-tmp"; + private static final OpenOption[] OPEN_OPTIONS = { + StandardOpenOption.DSYNC, // TODO: DSYNC or SYNC here? + StandardOpenOption.WRITE, + StandardOpenOption.TRUNCATE_EXISTING + }; + + private IOUtilities() {} + + /** + * Cleans up any temporary files which may have been created due to previous + * writes to {@link #atomicWrite(Path, WriteCallback)}. + * + * @param targetPath The target path to clean up temporary files in. + * @param prefix The prefix of temporary files to clean up, or null if all + * temporary files should be removed. + * + * @throws IOException if an I/O error occurs during deletion. + */ + public static void cleanupTempFiles(Path targetPath, @Nullable String prefix) throws IOException { + try (var filesToDelete = Files.find(targetPath, 1, createPredicate(prefix))) { + for (var file : filesToDelete.toList()) { + Files.deleteIfExists(file); + } + } + } + + private static BiPredicate createPredicate(@Nullable String prefix) { + return (file, attributes) -> { + final var fileName = file.getFileName().toString(); + return fileName.endsWith(TEMP_FILE_SUFFIX) && (prefix == null || fileName.startsWith(prefix)); + }; + } + + /** + * Behaves much the same as {@link NbtIo#writeCompressed(CompoundTag, Path)}, + * but uses {@link #atomicWrite(Path, WriteCallback)} behind the scenes to + * ensure the data is stored resilliently. + * + * @param tag The tag to write. + * @param path The path to write the NBT to. + * + * @throws IOException if an I/O error occurs during writing. + */ + public static void writeNbtCompressed(CompoundTag tag, Path path) throws IOException { + atomicWrite(path, stream -> { + try (var bufferedStream = new BufferedOutputStream(stream)) { + NbtIo.writeCompressed(tag, bufferedStream); + } + }); + } + + /** + * Behaves much the same as {@link NbtIo#write(CompoundTag, Path)}, + * but uses {@link #atomicWrite(Path, WriteCallback)} behind the scenes to + * ensure the data is stored resilliently. + * + * @param tag The tag to write. + * @param path The path to write the NBT to. + * + * @throws IOException if an I/O error occurs during writing. + */ + public static void writeNbt(CompoundTag tag, Path path) throws IOException { + atomicWrite(path, stream -> { + try (var bufferedStream = new BufferedOutputStream(stream); + var dataStream = new DataOutputStream(bufferedStream)) { + NbtIo.write(tag, dataStream); + } + }); + } + + /** + * Writes data to the given path "atomically", such that a crash will not + * leave the the file containing corrupted or otherwise half-written data. + *

+ * This method operates by creating a temporary file, writing to that file, + * and then moving the temporary file to the correct location after flushing. + * If a crash occurs during this process, the temporary file will be + * abandoned. + *

+ * Furthermore, the stream passed to {@code writeCallback} is not buffered, + * and it is the handler's responsibility to implement any buffering on top + * of this method. + * + * @param targetPath The desired path to write to. + * @param writeCallback A callback which receives the opened stream to write + * data to. + * + * @throws IOException if an I/O error occurs during writing. + */ + public static void atomicWrite(Path targetPath, WriteCallback writeCallback) throws IOException { + final var tempPath = Files.createTempFile( + targetPath.getParent(), + targetPath.getFileName().toString(), + TEMP_FILE_SUFFIX); + + try { + writeImpl(tempPath, targetPath, writeCallback); + } catch (Exception first) { + // If an exception occurs, we want to try and clean up if we can before rethrowing. + try { + Files.deleteIfExists(tempPath); + } catch (Exception second) { + // But if we can't, we suppress the exception. + first.addSuppressed(second); + } + + throw first; + } + } + + private static void writeImpl(Path tempFile, Path destination, WriteCallback callback) throws IOException { + try (OutputStream writeStream = Files.newOutputStream(tempFile, OPEN_OPTIONS)) { + callback.write(writeStream); + } + + // Now we try and move the file to the correct location, atomically if possible. + try { + Files.move(tempFile, destination, StandardCopyOption.ATOMIC_MOVE); + } catch (java.nio.file.AtomicMoveNotSupportedException e) { + Files.move(tempFile, destination, StandardCopyOption.REPLACE_EXISTING); + } + } + + /*** + * Declares an interface which is functionally equivalent to {@link Consumer}, + * except supports the ability to throw IOExceptions that may occur. + */ + public interface WriteCallback { + void write(OutputStream stream) throws IOException; + } +}