Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
import java.util.concurrent.SubmissionPublisher;
import java.util.logging.Level;
import java.util.stream.Collectors;

import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
import io.quarkus.deployment.builditem.HotDeploymentWatchedFileBuildItem;
import org.eclipse.microprofile.config.Config;
import org.eclipse.microprofile.config.ConfigProvider;
import org.jboss.logging.Logger;
Expand Down Expand Up @@ -44,7 +45,6 @@
import io.quarkus.deployment.builditem.ServiceStartBuildItem;
import io.quarkus.deployment.builditem.ShutdownContextBuildItem;
import io.quarkus.deployment.builditem.ShutdownListenerBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem;
import io.quarkus.deployment.logging.LogCleanupFilterBuildItem;
Expand Down Expand Up @@ -84,6 +84,14 @@
import io.vertx.core.impl.VertxImpl;
import io.vertx.ext.web.Router;

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.stream.Stream;

import io.quarkus.vertx.http.deployment.spi.GeneratedStaticResourceBuildItem;


class VertxHttpProcessor {

private static final String META_INF_SERVICES_EXCHANGE_ATTRIBUTE_BUILDER = "META-INF/services/io.quarkus.vertx.http.runtime.attribute.ExchangeAttributeBuilder";
Expand Down Expand Up @@ -208,6 +216,64 @@ public KubernetesPortBuildItem kubernetesForManagement(
managementBuildTimeConfig.enabled());
}

@BuildStep
void registerLocalStaticResources(VertxHttpBuildTimeConfig httpBuildTimeConfig,
BuildProducer<GeneratedStaticResourceBuildItem> generatedStaticResources) {

var localStatic = httpBuildTimeConfig.localStaticResources();
if (!localStatic.enabled()) {
return;
}

final String basePath = localStatic.normalizedPath();

Path root = Path.of(localStatic.directory()).normalize();
if (!Files.isDirectory(root)) {
return;
}

try (Stream<Path> paths = Files.walk(root)) {
paths.filter(Files::isRegularFile)
.forEach(file -> {
Path relative = root.relativize(file);
String relativeUnix = relative.toString().replace('\\', '/');

String endpoint = basePath + "/" + relativeUnix;
if (endpoint.endsWith("/")) {
endpoint = endpoint.substring(0, endpoint.length() - 1);
}

generatedStaticResources.produce(
new GeneratedStaticResourceBuildItem(endpoint, file));
});
} catch (IOException e) {
throw new UncheckedIOException("Failed to register local static resources from directory " + root, e);
}
}

@BuildStep
void watchLocalStaticResourcesForDev(VertxHttpBuildTimeConfig httpBuildTimeConfig,
BuildProducer<HotDeploymentWatchedFileBuildItem> watchedFiles) {

var localStatic = httpBuildTimeConfig.localStaticResources();
if (!localStatic.enabled()) {
return;
}

String dir = localStatic.directory();
if (dir == null || dir.isBlank()) {
return;
}

String normalizedDir = Path.of(dir).normalize().toAbsolutePath().toString();

watchedFiles.produce(HotDeploymentWatchedFileBuildItem.builder()
.setLocationPredicate(path -> path.startsWith(normalizedDir))
.setRestartNeeded(false)
.build());
}


@BuildStep
void notFoundRoutes(
List<RouteBuildItem> routes,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package io.quarkus.vertx.http;

import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.equalTo;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.QuarkusUnitTest;

public class LocalStaticResourcesTest {

@RegisterExtension
static final QuarkusUnitTest test = new QuarkusUnitTest()
.withConfigurationResource("application-local-static-resources.properties");

@Test
public void shouldServeLocalStaticResource() {
given()
.when().get("/local-static/test.txt")
.then()
.statusCode(200)
.body(equalTo("hello-static"));
}

@Test
public void shouldReturn404ForMissingResource() {
given()
.when().get("/local-static/does-not-exist.txt")
.then()
.statusCode(404);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
quarkus.http.local-static-resources.enabled=true
quarkus.http.local-static-resources.path=/local-static
quarkus.http.local-static-resources.directory=src/test/resources/local-static
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
hello-static
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package io.quarkus.vertx.http.runtime;

import io.quarkus.runtime.annotations.ConfigGroup;
import io.smallrye.config.WithDefault;

@ConfigGroup
public interface LocalStaticResourcesConfig {

/**
* Whether serving local static resources is enabled.
*/
@WithDefault("false")
boolean enabled();

/**
* The URL path under which the local directory is exposed,
* e.g. {@code /local-static/*}.
*/
@WithDefault("/local-static/*")
String path();

/**
* The local directory to serve files from, relative to the working directory.
* For example: {@code static/}.
*/
@WithDefault("static")
String directory();

/**
* Normalizes the configured path for serving local static resources.
* @return a normalized, safe base path for static resources
* @throws IllegalArgumentException if the path contains {@code *} or {@code ..}
*/
default String normalizedPath() {

String p = path().trim();

if (p.contains("*")) {
throw new IllegalArgumentException(
"quarkus.http.local-static-resources.path must not contain '*': " + p);
}

if (p.contains("..")) {
throw new IllegalArgumentException(
"quarkus.http.local-static-resources.path must not contain '..': " + p);
}

if (p.isEmpty()) {
return "/";
}

if (!p.startsWith("/")) {
p = "/" + p;
}

while (p.endsWith("/") && p.length() > 1) {
p = p.substring(0, p.length() - 1);
}

return p;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -115,4 +115,11 @@ public interface VertxHttpBuildTimeConfig {
* The compression level used when compression support is enabled.
*/
OptionalInt compressionLevel();

/**
* Static resources served directly from a local directory using a Vert.x {@code StaticHandler}.
*
* @return configuration for serving local static resources
*/
LocalStaticResourcesConfig localStaticResources();
}