diff --git a/cameleer-server-app/src/main/java/com/cameleer/server/app/storage/FilesystemArtifactStore.java b/cameleer-server-app/src/main/java/com/cameleer/server/app/storage/FilesystemArtifactStore.java index 9cbc6b8e..ee20fcca 100644 --- a/cameleer-server-app/src/main/java/com/cameleer/server/app/storage/FilesystemArtifactStore.java +++ b/cameleer-server-app/src/main/java/com/cameleer/server/app/storage/FilesystemArtifactStore.java @@ -7,6 +7,7 @@ import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardCopyOption; public class FilesystemArtifactStore implements ArtifactStore { @@ -24,9 +25,11 @@ public class FilesystemArtifactStore implements ArtifactStore { public String put(ArtifactCoordinates coords, InputStream bytes, long size) throws IOException { Path target = pathOf(coords); Files.createDirectories(target.getParent()); + Path tmp = target.resolveSibling(target.getFileName() + ".tmp"); try (InputStream in = bytes) { - Files.copy(in, target, java.nio.file.StandardCopyOption.REPLACE_EXISTING); + Files.copy(in, tmp, StandardCopyOption.REPLACE_EXISTING); } + Files.move(tmp, target, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); return target.toString(); } @@ -45,12 +48,17 @@ public class FilesystemArtifactStore implements ArtifactStore { Path target = pathOf(coords); Files.deleteIfExists(target); // Sweep empty {appId}/v{n}/ then {appId}/ if now empty. + // A concurrent put of a sibling version can create v{n+1}/ mid-sweep — + // DirectoryNotEmptyException just means "stop sweeping," which is what + // "remove empty parent dirs" semantically means. Path versionDir = target.getParent(); if (versionDir != null && Files.isDirectory(versionDir) && isEmpty(versionDir)) { - Files.delete(versionDir); + try { Files.delete(versionDir); } + catch (java.nio.file.DirectoryNotEmptyException e) { return; } Path appDir = versionDir.getParent(); if (appDir != null && Files.isDirectory(appDir) && isEmpty(appDir)) { - Files.delete(appDir); + try { Files.delete(appDir); } + catch (java.nio.file.DirectoryNotEmptyException e) { /* leave it */ } } } } diff --git a/cameleer-server-app/src/test/java/com/cameleer/server/app/storage/FilesystemArtifactStoreTest.java b/cameleer-server-app/src/test/java/com/cameleer/server/app/storage/FilesystemArtifactStoreTest.java index 1a52e6ef..250eb9e6 100644 --- a/cameleer-server-app/src/test/java/com/cameleer/server/app/storage/FilesystemArtifactStoreTest.java +++ b/cameleer-server-app/src/test/java/com/cameleer/server/app/storage/FilesystemArtifactStoreTest.java @@ -58,4 +58,35 @@ class FilesystemArtifactStoreTest { assertThat(store.locator(c)) .isEqualTo(tmp.resolve("11111111-1111-1111-1111-111111111111/v7/app.jar").toString()); } + + @Test + void putOverwritesExistingArtifactWithDifferentContent(@TempDir Path tmp) throws Exception { + FilesystemArtifactStore store = new FilesystemArtifactStore(tmp.toString()); + ArtifactCoordinates c = new ArtifactCoordinates("default", UUID.randomUUID(), 1); + + store.put(c, new ByteArrayInputStream("v1-bytes".getBytes()), 8); + store.put(c, new ByteArrayInputStream("v2-different-bytes".getBytes()), 18); + + try (InputStream in = store.get(c)) { + assertThat(new String(in.readAllBytes())).isEqualTo("v2-different-bytes"); + } + } + + @Test + void deleteLeavesAppDirAloneWhenSiblingVersionExists(@TempDir Path tmp) throws Exception { + FilesystemArtifactStore store = new FilesystemArtifactStore(tmp.toString()); + UUID appId = UUID.randomUUID(); + ArtifactCoordinates v1 = new ArtifactCoordinates("default", appId, 1); + ArtifactCoordinates v2 = new ArtifactCoordinates("default", appId, 2); + + store.put(v1, new ByteArrayInputStream("a".getBytes()), 1); + store.put(v2, new ByteArrayInputStream("b".getBytes()), 1); + + store.delete(v1); + + // v1/ should be gone, but v2/ and the appId/ dir should remain (sibling lives there) + assertThat(Files.exists(tmp.resolve(appId.toString()).resolve("v1"))).isFalse(); + assertThat(Files.exists(tmp.resolve(appId.toString()).resolve("v2/app.jar"))).isTrue(); + assertThat(Files.exists(tmp.resolve(appId.toString()))).isTrue(); + } }