mirror of https://github.com/halo-dev/halo
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
parent
9d0ad5de26
commit
77dd5b24dd
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
Loading…
Reference in New Issue