diff --git a/source/file-server/src/main/java/nl/aerius/fileserver/local/LocalFileStorageSevice.java b/source/file-server/src/main/java/nl/aerius/fileserver/local/LocalFileStorageSevice.java index 9645dc0..36b4f03 100644 --- a/source/file-server/src/main/java/nl/aerius/fileserver/local/LocalFileStorageSevice.java +++ b/source/file-server/src/main/java/nl/aerius/fileserver/local/LocalFileStorageSevice.java @@ -66,7 +66,17 @@ public void putFile(final String uuid, final String filename, final long size, f Files.createDirectory(uuidPath); } final Path file = filePath(uuidPath, filename); - Files.copy(in, file, StandardCopyOption.REPLACE_EXISTING); + // Write to a temporary file in the same directory and then atomically move it into place. A plain + // Files.copy(REPLACE_EXISTING) deletes the target before recreating it, which exposes a window where a + // concurrent read sees the file missing or partially written and fails with a 500. ATOMIC_MOVE (a rename) + // ensures readers always observe either the complete old file or the complete new one. + final Path tempFile = Files.createTempFile(uuidPath, filename + ".", ".tmp"); + try { + Files.copy(in, tempFile, StandardCopyOption.REPLACE_EXISTING); + Files.move(tempFile, file, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); + } finally { + Files.deleteIfExists(tempFile); + } } @Override diff --git a/source/file-server/src/test/java/nl/aerius/fileserver/local/LocalFileStorageSeviceTest.java b/source/file-server/src/test/java/nl/aerius/fileserver/local/LocalFileStorageSeviceTest.java index 8185e21..f504cfb 100644 --- a/source/file-server/src/test/java/nl/aerius/fileserver/local/LocalFileStorageSeviceTest.java +++ b/source/file-server/src/test/java/nl/aerius/fileserver/local/LocalFileStorageSeviceTest.java @@ -16,6 +16,7 @@ */ package nl.aerius.fileserver.local; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -82,6 +83,16 @@ void testPutFileOverwrite() throws IOException { assertEquals(overwriteContent, Files.readString(expectedFile.toPath()), "Content of file should be as expected."); } + @Test + void testPutFileLeavesNoTempFile() throws IOException { + // The atomic-write path stages the content in a temp file before moving it into place; ensure that + // temp file is always cleaned up so it can't leak into directory listings or repeated overwrites. + service.putFile(UUID_CODE, FILENAME, 10, null, new ByteArrayInputStream(CONTENT.getBytes())); + service.putFile(UUID_CODE, FILENAME, 0, null, new ByteArrayInputStream(CONTENT.getBytes())); + final String[] storedFiles = expectedFile.getParentFile().list(); + assertArrayEquals(new String[] {FILENAME}, storedFiles, "Only the target file should remain, no temp residue."); + } + @Test void testGetFile() throws IOException { writeTempFile();