Cache static resources, including plugin, theme, console, attachment (#3074)

#### 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
完善静态资源的缓存策略
```
pull/3076/head^2
John Niang 2022-12-29 18:14:32 +08:00 committed by GitHub
parent 9d0ad5de26
commit 77dd5b24dd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 52 additions and 23 deletions

View File

@ -8,7 +8,6 @@ import static run.halo.app.infra.utils.FileUtils.checkDirectoryTraversal;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URI; import java.net.URI;
import java.time.Duration; import java.time.Duration;
import java.time.Instant;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@ -105,7 +104,7 @@ public class WebFluxConfig implements WebFluxConfigurer {
var indexResource = applicationContext.getResource(indexLocation); var indexResource = applicationContext.getResource(indexLocation);
try { try {
return ServerResponse.ok() return ServerResponse.ok()
.lastModified(Instant.ofEpochMilli(indexResource.lastModified())) .cacheControl(CacheControl.noStore())
.body(BodyInserters.fromResource(indexResource)); .body(BodyInserters.fromResource(indexResource));
} catch (Throwable e) { } catch (Throwable e) {
return Mono.error(e); return Mono.error(e);
@ -119,16 +118,19 @@ public class WebFluxConfig implements WebFluxConfigurer {
@Override @Override
public void addResourceHandlers(ResourceHandlerRegistry registry) { public void addResourceHandlers(ResourceHandlerRegistry registry) {
var attachmentsRoot = haloProp.getWorkDir().resolve("attachments"); var attachmentsRoot = haloProp.getWorkDir().resolve("attachments");
var cacheControl = CacheControl.maxAge(Duration.ofDays(365 / 2));
// Mandatory resource mapping // Mandatory resource mapping
var uploadRegistration = registry.addResourceHandler("/upload/**") var uploadRegistration = registry.addResourceHandler("/upload/**")
.addResourceLocations(FILE_URL_PREFIX + attachmentsRoot.resolve("upload") + "/") .addResourceLocations(FILE_URL_PREFIX + attachmentsRoot.resolve("upload") + "/")
.setUseLastModified(true) .setUseLastModified(true)
.setCacheControl(CacheControl.maxAge(Duration.ofDays(365))); .setCacheControl(cacheControl);
// For console project // For console project
registry.addResourceHandler("/console/**") registry.addResourceHandler("/console/**")
.addResourceLocations(haloProp.getConsole().getLocation()) .addResourceLocations(haloProp.getConsole().getLocation())
.setCacheControl(cacheControl)
.setUseLastModified(true)
.resourceChain(true) .resourceChain(true)
.addResolver(new EncodedResourceResolver()) .addResolver(new EncodedResourceResolver())
.addResolver(new PathResourceResolver()); .addResolver(new PathResourceResolver());
@ -145,7 +147,9 @@ public class WebFluxConfig implements WebFluxConfigurer {
staticResource.getLocations().forEach(location -> { staticResource.getLocations().forEach(location -> {
var path = attachmentsRoot.resolve(location); var path = attachmentsRoot.resolve(location);
checkDirectoryTraversal(attachmentsRoot, path); checkDirectoryTraversal(attachmentsRoot, path);
registration.addResourceLocations(FILE_URL_PREFIX + path + "/"); registration.addResourceLocations(FILE_URL_PREFIX + path + "/")
.setCacheControl(cacheControl)
.setUseLastModified(true);
}); });
}); });

View File

@ -1,8 +1,13 @@
package run.halo.app.plugin; package run.halo.app.plugin;
import static run.halo.app.plugin.resources.BundleResourceUtils.getJsBundleResource;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.lang.reflect.Constructor; import java.lang.reflect.Constructor;
import java.nio.file.Path; import java.nio.file.Path;
import java.time.Duration;
import java.time.Instant;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.pf4j.ClassLoadingStrategy; import org.pf4j.ClassLoadingStrategy;
import org.pf4j.CompoundPluginLoader; 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.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; 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.util.StringUtils;
import org.springframework.web.reactive.accept.RequestedContentTypeResolver; 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.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions; import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse; 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. * Plugin autoconfiguration for Spring Boot.
@ -163,13 +169,20 @@ public class PluginAutoConfiguration {
String pluginName = request.pathVariable("name"); String pluginName = request.pathVariable("name");
String fileName = request.pathVariable("resource"); String fileName = request.pathVariable("resource");
Resource jsBundleResource = var jsBundle = getJsBundleResource(haloPluginManager, pluginName, fileName);
BundleResourceUtils.getJsBundleResource(haloPluginManager, pluginName, if (jsBundle == null || !jsBundle.exists()) {
fileName);
if (jsBundleResource == null) {
return ServerResponse.notFound().build(); 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(); .build();
} }

View File

@ -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.GET;
import static org.springframework.web.reactive.function.server.RequestPredicates.accept; 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.nio.file.Path;
import java.time.Duration;
import java.time.Instant;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.FileSystemResource;
import org.springframework.http.CacheControl;
import org.springframework.http.MediaType; 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.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse; 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.properties.HaloProperties;
import run.halo.app.infra.utils.FilePathUtils; import run.halo.app.infra.utils.FilePathUtils;
import run.halo.app.theme.dialect.LinkExpressionObjectDialect; import run.halo.app.theme.dialect.LinkExpressionObjectDialect;
@ -29,17 +35,23 @@ public class ThemeConfiguration {
@Bean @Bean
public RouterFunction<ServerResponse> themeAssets() { public RouterFunction<ServerResponse> themeAssets() {
return RouterFunctions return route(
.route(GET("/themes/{themeName}/assets/{*resource}") GET("/themes/{themeName}/assets/{*resource}").and(accept(MediaType.TEXT_PLAIN)),
.and(accept(MediaType.TEXT_HTML)), request -> {
request -> { var themeName = request.pathVariable("themeName");
String themeName = request.pathVariable("themeName"); var resource = request.pathVariable("resource");
String resource = request.pathVariable("resource"); var fsRes = new FileSystemResource(getThemeAssetsPath(themeName, resource));
FileSystemResource fileSystemResource = try {
new FileSystemResource(getThemeAssetsPath(themeName, resource)); var lastModified = Instant.ofEpochMilli(fsRes.lastModified());
return ServerResponse.ok() return request.checkNotModified(lastModified)
.bodyValue(fileSystemResource); .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) { private Path getThemeAssetsPath(String themeName, String resource) {