From 77dd5b24ddba958ce95a9877d04711d41b618b0a Mon Sep 17 00:00:00 2001 From: John Niang Date: Thu, 29 Dec 2022 18:14:32 +0800 Subject: [PATCH] Cache static resources, including plugin, theme, console, attachment (#3074) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### What type of PR is this? /kind improvement /area core /milestone 2.1.x #### What this PR does / why we need it: Add cache control and last modified headers for responses of static resources. #### Which issue(s) this PR fixes: Fixes https://github.com/halo-dev/halo/issues/2966 #### Special notes for your reviewer: We can check the response header of statis resources to know if the cache control is working correctly. #### Does this PR introduce a user-facing change? ```release-note 完善静态资源的缓存策略 ``` --- .../run/halo/app/config/WebFluxConfig.java | 12 ++++--- .../app/plugin/PluginAutoConfiguration.java | 27 ++++++++++---- .../halo/app/theme/ThemeConfiguration.java | 36 ++++++++++++------- 3 files changed, 52 insertions(+), 23 deletions(-) diff --git a/src/main/java/run/halo/app/config/WebFluxConfig.java b/src/main/java/run/halo/app/config/WebFluxConfig.java index c07806035..a504b2ad0 100644 --- a/src/main/java/run/halo/app/config/WebFluxConfig.java +++ b/src/main/java/run/halo/app/config/WebFluxConfig.java @@ -8,7 +8,6 @@ import static run.halo.app.infra.utils.FileUtils.checkDirectoryTraversal; import com.fasterxml.jackson.databind.ObjectMapper; import java.net.URI; import java.time.Duration; -import java.time.Instant; import java.util.List; import java.util.Objects; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -105,7 +104,7 @@ public class WebFluxConfig implements WebFluxConfigurer { var indexResource = applicationContext.getResource(indexLocation); try { return ServerResponse.ok() - .lastModified(Instant.ofEpochMilli(indexResource.lastModified())) + .cacheControl(CacheControl.noStore()) .body(BodyInserters.fromResource(indexResource)); } catch (Throwable e) { return Mono.error(e); @@ -119,16 +118,19 @@ public class WebFluxConfig implements WebFluxConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { var attachmentsRoot = haloProp.getWorkDir().resolve("attachments"); + var cacheControl = CacheControl.maxAge(Duration.ofDays(365 / 2)); // Mandatory resource mapping var uploadRegistration = registry.addResourceHandler("/upload/**") .addResourceLocations(FILE_URL_PREFIX + attachmentsRoot.resolve("upload") + "/") .setUseLastModified(true) - .setCacheControl(CacheControl.maxAge(Duration.ofDays(365))); + .setCacheControl(cacheControl); // For console project registry.addResourceHandler("/console/**") .addResourceLocations(haloProp.getConsole().getLocation()) + .setCacheControl(cacheControl) + .setUseLastModified(true) .resourceChain(true) .addResolver(new EncodedResourceResolver()) .addResolver(new PathResourceResolver()); @@ -145,7 +147,9 @@ public class WebFluxConfig implements WebFluxConfigurer { staticResource.getLocations().forEach(location -> { var path = attachmentsRoot.resolve(location); checkDirectoryTraversal(attachmentsRoot, path); - registration.addResourceLocations(FILE_URL_PREFIX + path + "/"); + registration.addResourceLocations(FILE_URL_PREFIX + path + "/") + .setCacheControl(cacheControl) + .setUseLastModified(true); }); }); diff --git a/src/main/java/run/halo/app/plugin/PluginAutoConfiguration.java b/src/main/java/run/halo/app/plugin/PluginAutoConfiguration.java index 54027c4f9..fdff6fb24 100644 --- a/src/main/java/run/halo/app/plugin/PluginAutoConfiguration.java +++ b/src/main/java/run/halo/app/plugin/PluginAutoConfiguration.java @@ -1,8 +1,13 @@ package run.halo.app.plugin; +import static run.halo.app.plugin.resources.BundleResourceUtils.getJsBundleResource; + import java.io.File; +import java.io.IOException; import java.lang.reflect.Constructor; import java.nio.file.Path; +import java.time.Duration; +import java.time.Instant; import lombok.extern.slf4j.Slf4j; import org.pf4j.ClassLoadingStrategy; import org.pf4j.CompoundPluginLoader; @@ -22,13 +27,14 @@ import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.core.io.Resource; +import org.springframework.http.CacheControl; import org.springframework.util.StringUtils; import org.springframework.web.reactive.accept.RequestedContentTypeResolver; +import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.RouterFunctions; import org.springframework.web.reactive.function.server.ServerResponse; -import run.halo.app.plugin.resources.BundleResourceUtils; +import reactor.core.publisher.Mono; /** * Plugin autoconfiguration for Spring Boot. @@ -163,13 +169,20 @@ public class PluginAutoConfiguration { String pluginName = request.pathVariable("name"); String fileName = request.pathVariable("resource"); - Resource jsBundleResource = - BundleResourceUtils.getJsBundleResource(haloPluginManager, pluginName, - fileName); - if (jsBundleResource == null) { + var jsBundle = getJsBundleResource(haloPluginManager, pluginName, fileName); + if (jsBundle == null || !jsBundle.exists()) { return ServerResponse.notFound().build(); } - return ServerResponse.ok().bodyValue(jsBundleResource); + try { + var lastModified = Instant.ofEpochMilli(jsBundle.lastModified()); + return request.checkNotModified(lastModified) + .switchIfEmpty(Mono.defer(() -> ServerResponse.ok() + .cacheControl(CacheControl.maxAge(Duration.ofDays(365 / 2))) + .lastModified(lastModified) + .body(BodyInserters.fromResource(jsBundle)))); + } catch (IOException e) { + throw new RuntimeException(e); + } }) .build(); } diff --git a/src/main/java/run/halo/app/theme/ThemeConfiguration.java b/src/main/java/run/halo/app/theme/ThemeConfiguration.java index 88fc83e5e..45a3d89fe 100644 --- a/src/main/java/run/halo/app/theme/ThemeConfiguration.java +++ b/src/main/java/run/halo/app/theme/ThemeConfiguration.java @@ -2,15 +2,21 @@ package run.halo.app.theme; import static org.springframework.web.reactive.function.server.RequestPredicates.GET; import static org.springframework.web.reactive.function.server.RequestPredicates.accept; +import static org.springframework.web.reactive.function.server.RouterFunctions.route; +import java.io.IOException; import java.nio.file.Path; +import java.time.Duration; +import java.time.Instant; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.FileSystemResource; +import org.springframework.http.CacheControl; import org.springframework.http.MediaType; +import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.server.RouterFunction; -import org.springframework.web.reactive.function.server.RouterFunctions; import org.springframework.web.reactive.function.server.ServerResponse; +import reactor.core.publisher.Mono; import run.halo.app.infra.properties.HaloProperties; import run.halo.app.infra.utils.FilePathUtils; import run.halo.app.theme.dialect.LinkExpressionObjectDialect; @@ -29,17 +35,23 @@ public class ThemeConfiguration { @Bean public RouterFunction themeAssets() { - return RouterFunctions - .route(GET("/themes/{themeName}/assets/{*resource}") - .and(accept(MediaType.TEXT_HTML)), - request -> { - String themeName = request.pathVariable("themeName"); - String resource = request.pathVariable("resource"); - FileSystemResource fileSystemResource = - new FileSystemResource(getThemeAssetsPath(themeName, resource)); - return ServerResponse.ok() - .bodyValue(fileSystemResource); - }); + return route( + GET("/themes/{themeName}/assets/{*resource}").and(accept(MediaType.TEXT_PLAIN)), + request -> { + var themeName = request.pathVariable("themeName"); + var resource = request.pathVariable("resource"); + var fsRes = new FileSystemResource(getThemeAssetsPath(themeName, resource)); + try { + var lastModified = Instant.ofEpochMilli(fsRes.lastModified()); + return request.checkNotModified(lastModified) + .switchIfEmpty(Mono.defer(() -> ServerResponse.ok() + .cacheControl(CacheControl.maxAge(Duration.ofDays(356 / 2))) + .lastModified(lastModified) + .body(BodyInserters.fromResource(fsRes)))); + } catch (IOException e) { + return Mono.error(e); + } + }); } private Path getThemeAssetsPath(String themeName, String resource) {