diff --git a/configurate-core/src/main/java/ninja/leaping/configurate/loader/AtomicFiles.java b/configurate-core/src/main/java/ninja/leaping/configurate/loader/AtomicFiles.java index 640055d8b..e1faea2b4 100644 --- a/configurate-core/src/main/java/ninja/leaping/configurate/loader/AtomicFiles.java +++ b/configurate-core/src/main/java/ninja/leaping/configurate/loader/AtomicFiles.java @@ -64,9 +64,19 @@ public static Callable createAtomicWriterFactory(@NonNull Path p */ @NonNull public static BufferedWriter createAtomicBufferedWriter(@NonNull Path path, @NonNull Charset charset) throws IOException { + // absolute path = path.toAbsolutePath(); - Path writePath = getTemporaryPath(path.getParent(), path.getFileName().toString()); + // unwrap any symbolic links + try { + while (Files.isSymbolicLink(path)) { + path = Files.readSymbolicLink(path); + } + } catch (final UnsupportedOperationException | IOException ex) { + // ignore, FS probably doesn't support symlinks + } + + final Path writePath = getTemporaryPath(path.getParent(), path.getFileName().toString()); if (Files.exists(path)) { Files.copy(path, writePath, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING); } diff --git a/configurate-core/src/test/java/ninja/leaping/configurate/loader/AbstractConfigurationLoaderTest.java b/configurate-core/src/test/java/ninja/leaping/configurate/loader/AbstractConfigurationLoaderTest.java index e78f85be9..78049a719 100644 --- a/configurate-core/src/test/java/ninja/leaping/configurate/loader/AbstractConfigurationLoaderTest.java +++ b/configurate-core/src/test/java/ninja/leaping/configurate/loader/AbstractConfigurationLoaderTest.java @@ -16,13 +16,21 @@ */ package ninja.leaping.configurate.loader; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junitpioneer.jupiter.TempDirectory; +import java.io.BufferedWriter; import java.io.File; import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.stream.Collectors; @ExtendWith(TempDirectory.class) public class AbstractConfigurationLoaderTest { @@ -40,4 +48,30 @@ public void testLoadNonexistantFile(@TempDirectory.TempDir Path tempDir) throws TestConfigurationLoader loader = TestConfigurationLoader.builder().setFile(tempFile).build(); loader.load(); } + + @Test + public void testSaveFollowsSymbolicLinks(final @TempDirectory.TempDir Path tempDir) throws IOException { + final Path actualFile = tempDir.resolve(Paths.get("first", "second", "third.json")); + Files.createDirectories(actualFile.getParent()); + final Path layerOne = tempDir.resolve("general.json"); + final Path layerTwo = tempDir.resolve("general2.json"); + + Files.createFile(actualFile); + Files.createSymbolicLink(layerOne, actualFile); + Files.createSymbolicLink(layerTwo, layerOne); + + try (BufferedWriter writer = AtomicFiles.createAtomicBufferedWriter(layerTwo, StandardCharsets.UTF_8)) { + writer.write("I should follow symlinks!\n"); + } + + // We expect links are preserved, and the underlying file is written to + assertTrue(Files.isSymbolicLink(layerTwo)); + assertTrue(Files.isSymbolicLink(layerOne)); + assertEquals(layerOne, Files.readSymbolicLink(layerTwo)); + assertEquals(actualFile, Files.readSymbolicLink(layerOne)); + assertEquals("I should follow symlinks!", Files.readAllLines(layerTwo, StandardCharsets.UTF_8).stream() + .collect(Collectors.joining("\n"))); + assertEquals("I should follow symlinks!", Files.readAllLines(actualFile, StandardCharsets.UTF_8).stream() + .collect(Collectors.joining("\n"))); + } }