refactor: add version flag to plugin static assets (#3381)

#### What type of PR is this?
/kind improvement
/area core
/milestone 2.3.x

#### What this PR does / why we need it:
对插件的静态资源(js, css) 增加 version 版本号,用于解决插件静态资源缓存的问题。另外同时也对插件 Logo 增加了版本号标识。

#### Which issue(s) this PR fixes:
Fixes #3205 

#### Special notes for your reviewer:
验证 Console 端插件静态资源链接是否携带 `version={pluginVersion}` 。

特别的,对于 logo只会增加相对路径下的地址,绝对路径的地址将直接返回原始路径/值。

#### Does this PR introduce a user-facing change?

```release-note
为插件静态资源路径增加版本标识以解决缓存更新不及时的问题
```
pull/3363/head^2
Li 2023-02-24 18:02:38 +08:00 committed by GitHub
parent c8f3229cd6
commit 3330ff8c3d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 106 additions and 10 deletions

View File

@ -29,9 +29,11 @@ import org.springframework.context.ApplicationEventPublisher;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.web.util.UriComponentsBuilder;
import run.halo.app.core.extension.Plugin;
import run.halo.app.core.extension.ReverseProxy;
import run.halo.app.core.extension.Setting;
@ -108,20 +110,12 @@ public class PluginReconciler implements Reconciler<Request> {
}
createInitialReverseProxyIfNotPresent(plugin);
Plugin.PluginStatus status = plugin.statusNonNull();
// filled logo path
String logo = plugin.getSpec().getLogo();
if (PathUtils.isAbsoluteUri(logo)) {
status.setLogo(logo);
} else {
String assetsPrefix =
PluginConst.assertsRoutePrefix(plugin.getMetadata().getName());
status.setLogo(PathUtils.combinePath(assetsPrefix, logo));
}
generateAccessibleLogoUrl(plugin);
// update phase
PluginWrapper pluginWrapper = getPluginWrapper(name);
Plugin.PluginStatus status = plugin.statusNonNull();
status.setPhase(pluginWrapper.getPluginState());
updateStatus(plugin.getMetadata().getName(), status);
return false;
@ -129,6 +123,23 @@ public class PluginReconciler implements Reconciler<Request> {
.orElse(false);
}
void generateAccessibleLogoUrl(Plugin plugin) {
String logo = plugin.getSpec().getLogo();
if (StringUtils.isBlank(logo)) {
return;
}
Plugin.PluginStatus status = plugin.statusNonNull();
if (PathUtils.isAbsoluteUri(logo)) {
status.setLogo(logo);
} else {
String assetsPrefix =
PluginConst.assertsRoutePrefix(plugin.getMetadata().getName());
String versionedLogo =
applyVersioningToStaticResource(logo, plugin.getSpec().getVersion());
status.setLogo(PathUtils.combinePath(assetsPrefix, versionedLogo));
}
}
Optional<Setting> lookupPluginSetting(String name, String settingName) {
Assert.notNull(name, "Plugin name must not be null");
Assert.notNull(settingName, "Setting name must not be null");
@ -374,12 +385,15 @@ public class PluginReconciler implements Reconciler<Request> {
plugin.statusNonNull().setLastStartTime(Instant.now());
final String pluginVersion = plugin.getSpec().getVersion();
String jsBundlePath =
BundleResourceUtils.getJsBundlePath(haloPluginManager, name);
jsBundlePath = applyVersioningToStaticResource(jsBundlePath, pluginVersion);
status.setEntry(jsBundlePath);
String cssBundlePath =
BundleResourceUtils.getCssBundlePath(haloPluginManager, name);
cssBundlePath = applyVersioningToStaticResource(cssBundlePath, pluginVersion);
status.setStylesheet(cssBundlePath);
status.setPhase(currentState);
@ -398,6 +412,15 @@ public class PluginReconciler implements Reconciler<Request> {
});
}
private String applyVersioningToStaticResource(@Nullable String path, String pluginVersion) {
if (StringUtils.isNotBlank(path)) {
return UriComponentsBuilder.fromUriString(path)
.queryParam("version", pluginVersion)
.build().toString();
}
return path;
}
PluginStartingError getStaringErrorInfo(String name) {
PluginStartingError startingError =
haloPluginManager.getPluginStartingError(name);

View File

@ -18,6 +18,7 @@ import java.util.Optional;
import org.json.JSONException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
@ -287,6 +288,78 @@ class PluginReconcilerTest {
verify(extensionClient, times(1)).update(any());
}
@Nested
class PluginLogoTest {
@Test
void absoluteUri() {
Plugin plugin = new Plugin();
plugin.setSpec(new Plugin.PluginSpec());
plugin.getSpec().setLogo("https://example.com/logo.png");
plugin.getSpec().setVersion("1.0.0");
pluginReconciler.generateAccessibleLogoUrl(plugin);
assertThat(plugin.statusNonNull().getLogo())
.isEqualTo("https://example.com/logo.png");
}
@Test
void absoluteUriWithQueryParam() {
Plugin plugin = new Plugin();
plugin.setSpec(new Plugin.PluginSpec());
plugin.getSpec().setLogo("https://example.com/logo.png?hello=world");
plugin.getSpec().setVersion("1.0.0");
pluginReconciler.generateAccessibleLogoUrl(plugin);
assertThat(plugin.statusNonNull().getLogo())
.isEqualTo("https://example.com/logo.png?hello=world");
}
@Test
void logoIsNull() {
Plugin plugin = new Plugin();
plugin.setSpec(new Plugin.PluginSpec());
plugin.getSpec().setLogo(null);
plugin.getSpec().setVersion("1.0.0");
pluginReconciler.generateAccessibleLogoUrl(plugin);
assertThat(plugin.statusNonNull().getLogo()).isNull();
}
@Test
void logoIsEmpty() {
Plugin plugin = new Plugin();
plugin.setSpec(new Plugin.PluginSpec());
plugin.getSpec().setLogo("");
plugin.getSpec().setVersion("1.0.0");
pluginReconciler.generateAccessibleLogoUrl(plugin);
assertThat(plugin.statusNonNull().getLogo()).isNull();
}
@Test
void relativePath() {
Plugin plugin = new Plugin();
plugin.setSpec(new Plugin.PluginSpec());
plugin.setMetadata(new Metadata());
plugin.getMetadata().setName("fake-plugin");
plugin.getSpec().setLogo("/static/logo.jpg");
plugin.getSpec().setVersion("1.0.0");
pluginReconciler.generateAccessibleLogoUrl(plugin);
assertThat(plugin.statusNonNull().getLogo())
.isEqualTo("/plugins/fake-plugin/assets/static/logo.jpg?version=1.0.0");
}
@Test
void dataBlob() {
Plugin plugin = new Plugin();
plugin.setSpec(new Plugin.PluginSpec());
plugin.setMetadata(new Metadata());
plugin.getMetadata().setName("fake-plugin");
plugin.getSpec().setLogo("data:image/gif;base64,R0lGODfake");
plugin.getSpec().setVersion("2.0.0");
pluginReconciler.generateAccessibleLogoUrl(plugin);
assertThat(plugin.statusNonNull().getLogo())
.isEqualTo("data:image/gif;base64,R0lGODfake");
}
}
private ArgumentCaptor<Plugin> doReconcileNeedRequeue() {
ArgumentCaptor<Plugin> pluginCaptor = ArgumentCaptor.forClass(Plugin.class);
doNothing().when(extensionClient).update(pluginCaptor.capture());