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 new file mode 100644 index 00000000..9cbc6b8e --- /dev/null +++ b/cameleer-server-app/src/main/java/com/cameleer/server/app/storage/FilesystemArtifactStore.java @@ -0,0 +1,68 @@ +package com.cameleer.server.app.storage; + +import com.cameleer.server.core.storage.ArtifactCoordinates; +import com.cameleer.server.core.storage.ArtifactStore; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; + +public class FilesystemArtifactStore implements ArtifactStore { + + private final Path root; + + public FilesystemArtifactStore(String root) { + this.root = Path.of(root); + } + + private Path pathOf(ArtifactCoordinates coords) { + return root.resolve(coords.filesystemKey()); + } + + @Override + public String put(ArtifactCoordinates coords, InputStream bytes, long size) throws IOException { + Path target = pathOf(coords); + Files.createDirectories(target.getParent()); + try (InputStream in = bytes) { + Files.copy(in, target, java.nio.file.StandardCopyOption.REPLACE_EXISTING); + } + return target.toString(); + } + + @Override + public InputStream get(ArtifactCoordinates coords) throws IOException { + return Files.newInputStream(pathOf(coords)); + } + + @Override + public boolean exists(ArtifactCoordinates coords) { + return Files.exists(pathOf(coords)); + } + + @Override + public void delete(ArtifactCoordinates coords) throws IOException { + Path target = pathOf(coords); + Files.deleteIfExists(target); + // Sweep empty {appId}/v{n}/ then {appId}/ if now empty. + Path versionDir = target.getParent(); + if (versionDir != null && Files.isDirectory(versionDir) && isEmpty(versionDir)) { + Files.delete(versionDir); + Path appDir = versionDir.getParent(); + if (appDir != null && Files.isDirectory(appDir) && isEmpty(appDir)) { + Files.delete(appDir); + } + } + } + + @Override + public String locator(ArtifactCoordinates coords) { + return pathOf(coords).toString(); + } + + private static boolean isEmpty(Path dir) throws IOException { + try (var entries = Files.list(dir)) { + return entries.findFirst().isEmpty(); + } + } +} 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 new file mode 100644 index 00000000..1a52e6ef --- /dev/null +++ b/cameleer-server-app/src/test/java/com/cameleer/server/app/storage/FilesystemArtifactStoreTest.java @@ -0,0 +1,61 @@ +package com.cameleer.server.app.storage; + +import com.cameleer.server.core.storage.ArtifactCoordinates; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +class FilesystemArtifactStoreTest { + + @Test + void roundTripsBytes(@TempDir Path tmp) throws Exception { + FilesystemArtifactStore store = new FilesystemArtifactStore(tmp.toString()); + ArtifactCoordinates c = new ArtifactCoordinates("default", UUID.randomUUID(), 1); + + byte[] payload = "hello-jar".getBytes(); + store.put(c, new ByteArrayInputStream(payload), payload.length); + + assertThat(store.exists(c)).isTrue(); + try (InputStream in = store.get(c)) { + assertThat(in.readAllBytes()).isEqualTo(payload); + } + } + + @Test + void deleteRemovesFileAndEmptyParents(@TempDir Path tmp) throws Exception { + FilesystemArtifactStore store = new FilesystemArtifactStore(tmp.toString()); + UUID appId = UUID.randomUUID(); + ArtifactCoordinates c = new ArtifactCoordinates("default", appId, 1); + store.put(c, new ByteArrayInputStream("x".getBytes()), 1); + + store.delete(c); + + assertThat(store.exists(c)).isFalse(); + // {tmp}/{appId}/v1/ should be gone too + assertThat(Files.exists(tmp.resolve(appId.toString()).resolve("v1"))).isFalse(); + } + + @Test + void deleteIsIdempotent(@TempDir Path tmp) throws Exception { + FilesystemArtifactStore store = new FilesystemArtifactStore(tmp.toString()); + ArtifactCoordinates c = new ArtifactCoordinates("default", UUID.randomUUID(), 1); + store.delete(c); // no exception + assertThat(store.exists(c)).isFalse(); + } + + @Test + void locatorMatchesLegacyAbsolutePath(@TempDir Path tmp) { + FilesystemArtifactStore store = new FilesystemArtifactStore(tmp.toString()); + UUID appId = UUID.fromString("11111111-1111-1111-1111-111111111111"); + ArtifactCoordinates c = new ArtifactCoordinates("default", appId, 7); + assertThat(store.locator(c)) + .isEqualTo(tmp.resolve("11111111-1111-1111-1111-111111111111/v7/app.jar").toString()); + } +}