refactor(web): authoritative Content-Length, typed Optional<AppVersion> in controller

This commit is contained in:
hsiegeln
2026-04-27 15:47:37 +02:00
parent 433155ae0c
commit 940bf18aba
7 changed files with 43 additions and 13 deletions

View File

@@ -2,7 +2,6 @@ package com.cameleer.server.app.security;
import com.cameleer.server.app.web.ArtifactDownloadTokenSigner;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -39,9 +38,8 @@ public class SecurityBeanConfig {
}
@Bean
public ArtifactDownloadTokenSigner artifactDownloadTokenSigner(
@Value("${cameleer.server.security.jwtsecret}") String jwtSecret) {
return new ArtifactDownloadTokenSigner(jwtSecret, Clock.systemUTC());
public ArtifactDownloadTokenSigner artifactDownloadTokenSigner(SecurityProperties properties) {
return new ArtifactDownloadTokenSigner(properties.getJwtSecret(), Clock.systemUTC());
}
@Bean

View File

@@ -43,6 +43,11 @@ public class FilesystemArtifactStore implements ArtifactStore {
return Files.newInputStream(pathOf(coords));
}
@Override
public long size(ArtifactCoordinates coords) throws IOException {
return Files.size(pathOf(coords));
}
@Override
public boolean exists(ArtifactCoordinates coords) {
return Files.exists(pathOf(coords));

View File

@@ -2,6 +2,7 @@ package com.cameleer.server.app.web;
import com.cameleer.server.core.runtime.AppService;
import com.cameleer.server.core.runtime.AppVersion;
import com.cameleer.server.core.storage.ArtifactCoordinates;
import com.cameleer.server.core.storage.ArtifactStore;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.MediaType;
@@ -14,6 +15,7 @@ import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
import java.io.InputStream;
import java.util.Optional;
import java.util.UUID;
/**
@@ -44,16 +46,17 @@ public class ArtifactDownloadController {
if (!signer.verify(appVersionId, exp, sig)) {
return ResponseEntity.status(401).build();
}
AppVersion version;
try {
version = appService.getVersion(appVersionId);
} catch (IllegalArgumentException e) {
Optional<AppVersion> versionOpt = appService.findVersion(appVersionId);
if (versionOpt.isEmpty()) {
return ResponseEntity.notFound().build();
}
InputStream in = artifactStore.get(appService.coordinatesFor(version));
AppVersion version = versionOpt.get();
ArtifactCoordinates coords = appService.coordinatesFor(version);
long size = artifactStore.size(coords);
InputStream in = artifactStore.get(coords);
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType("application/java-archive"))
.contentLength(version.jarSizeBytes())
.contentLength(size)
.body(new InputStreamResource(in));
}
}

View File

@@ -72,6 +72,15 @@ class FilesystemArtifactStoreTest {
}
}
@Test
void sizeReturnsActualOnDiskBytes(@TempDir Path tmp) throws Exception {
FilesystemArtifactStore store = new FilesystemArtifactStore(tmp.toString());
ArtifactCoordinates c = new ArtifactCoordinates("default", UUID.randomUUID(), 1);
byte[] payload = new byte[1234];
store.put(c, new java.io.ByteArrayInputStream(payload), payload.length);
assertThat(store.size(c)).isEqualTo(1234L);
}
@Test
void deleteLeavesAppDirAloneWhenSiblingVersionExists(@TempDir Path tmp) throws Exception {
FilesystemArtifactStore store = new FilesystemArtifactStore(tmp.toString());

View File

@@ -10,6 +10,7 @@ import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import java.io.ByteArrayInputStream;
import java.util.Optional;
import java.util.UUID;
import static org.mockito.ArgumentMatchers.any;
@@ -54,8 +55,9 @@ class ArtifactDownloadControllerTest {
UUID id = UUID.randomUUID();
when(signer.verify(eq(id), eq(123L), eq("good"))).thenReturn(true);
AppVersion v = new AppVersion(id, UUID.randomUUID(), 1, "loc", "h", "a.jar", 5L, null, null, null);
when(appService.getVersion(id)).thenReturn(v);
when(appService.findVersion(id)).thenReturn(Optional.of(v));
when(appService.coordinatesFor(v)).thenReturn(new ArtifactCoordinates("default", v.appId(), 1));
when(artifactStore.size(any())).thenReturn(5L);
when(artifactStore.get(any())).thenReturn(new ByteArrayInputStream("hello".getBytes()));
mvc.perform(get("/api/v1/artifacts/{id}", id).param("exp", "123").param("sig", "good"))
@@ -73,14 +75,14 @@ class ArtifactDownloadControllerTest {
.andExpect(status().isUnauthorized());
// Defence-in-depth: bad sig must short-circuit before any data lookup.
verify(appService, never()).getVersion(any());
verify(appService, never()).findVersion(any());
}
@Test
void returns404WhenArtifactMissing() throws Exception {
UUID id = UUID.randomUUID();
when(signer.verify(any(), anyLong(), any())).thenReturn(true);
when(appService.getVersion(id)).thenThrow(new IllegalArgumentException("AppVersion not found"));
when(appService.findVersion(id)).thenReturn(Optional.empty());
mvc.perform(get("/api/v1/artifacts/{id}", id).param("exp", "123").param("sig", "ok"))
.andExpect(status().isNotFound());