mirror of https://github.com/halo-dev/halo
feat: add theme uninstall endpoint (#2315)
<!-- Thanks for sending a pull request! Here are some tips for you: 1. 如果这是你的第一次,请阅读我们的贡献指南:<https://github.com/halo-dev/halo/blob/master/CONTRIBUTING.md>。 1. If this is your first time, please read our contributor guidelines: <https://github.com/halo-dev/halo/blob/master/CONTRIBUTING.md>. 2. 请根据你解决问题的类型为 Pull Request 添加合适的标签。 2. Please label this pull request according to what type of issue you are addressing, especially if this is a release targeted pull request. 3. 请确保你已经添加并运行了适当的测试。 3. Ensure you have added or ran the appropriate tests for your PR. --> #### What type of PR is this? /kind feature /milestone 2.0 /area core /kind api-change <!-- 添加其中一个类别: Add one of the following kinds: /kind bug /kind cleanup /kind documentation /kind feature /kind improvement 适当添加其中一个或多个类别(可选): Optionally add one or more of the following kinds if applicable: /kind api-change /kind deprecation /kind failing-test /kind flake /kind regression --> #### What this PR does / why we need it: 新增主题卸载 endpoint #### Which issue(s) this PR fixes: <!-- PR 合并时自动关闭 issue。 Automatically closes linked issue when PR is merged. 用法:`Fixes #<issue 号>`,或者 `Fixes (粘贴 issue 完整链接)` Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`. --> Fixes #2306 #### Special notes for your reviewer: /cc @halo-dev/sig-halo #### Does this PR introduce a user-facing change? <!-- 如果当前 Pull Request 的修改不会造成用户侧的任何变更,在 `release-note` 代码块儿中填写 `NONE`。 否则请填写用户侧能够理解的 Release Note。如果当前 Pull Request 包含破坏性更新(Break Change), Release Note 需要以 `action required` 开头。 If no, just write "NONE" in the release-note block below. If yes, a release note is required: Enter your extended release note in the block below. If the PR requires additional action from users switching to the new release, include the string "action required". --> ```release-note None ```pull/2303/head^2
parent
349db687e3
commit
7026681747
|
@ -9,10 +9,12 @@ import org.springframework.web.reactive.function.server.ServerResponse;
|
|||
import run.halo.app.core.extension.Plugin;
|
||||
import run.halo.app.core.extension.Role;
|
||||
import run.halo.app.core.extension.RoleBinding;
|
||||
import run.halo.app.core.extension.Theme;
|
||||
import run.halo.app.core.extension.User;
|
||||
import run.halo.app.core.extension.reconciler.PluginReconciler;
|
||||
import run.halo.app.core.extension.reconciler.RoleBindingReconciler;
|
||||
import run.halo.app.core.extension.reconciler.RoleReconciler;
|
||||
import run.halo.app.core.extension.reconciler.ThemeReconciler;
|
||||
import run.halo.app.core.extension.reconciler.UserReconciler;
|
||||
import run.halo.app.core.extension.service.RoleService;
|
||||
import run.halo.app.extension.DefaultExtensionClient;
|
||||
|
@ -28,6 +30,7 @@ import run.halo.app.extension.controller.ControllerBuilder;
|
|||
import run.halo.app.extension.controller.ControllerManager;
|
||||
import run.halo.app.extension.router.ExtensionCompositeRouterFunction;
|
||||
import run.halo.app.extension.store.ExtensionStoreClient;
|
||||
import run.halo.app.infra.properties.HaloProperties;
|
||||
import run.halo.app.plugin.HaloPluginManager;
|
||||
import run.halo.app.plugin.resources.JsBundleRuleProvider;
|
||||
|
||||
|
@ -99,6 +102,14 @@ public class ExtensionConfiguration {
|
|||
.extension(new Plugin())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
Controller themeController(ExtensionClient client, HaloProperties haloProperties) {
|
||||
return new ControllerBuilder("theme-controller", client)
|
||||
.reconciler(new ThemeReconciler(client, haloProperties))
|
||||
.extension(new Theme())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@ import run.halo.app.core.extension.Theme;
|
|||
import run.halo.app.extension.ConfigMap;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.Unstructured;
|
||||
import run.halo.app.infra.ThemeInstallationException;
|
||||
import run.halo.app.infra.exception.ThemeInstallationException;
|
||||
import run.halo.app.infra.properties.HaloProperties;
|
||||
import run.halo.app.infra.utils.FileUtils;
|
||||
import run.halo.app.infra.utils.YamlUnstructuredLoader;
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
package run.halo.app.core.extension.reconciler;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.util.FileSystemUtils;
|
||||
import run.halo.app.core.extension.Setting;
|
||||
import run.halo.app.core.extension.Theme;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.controller.Reconciler;
|
||||
import run.halo.app.infra.exception.ThemeUninstallException;
|
||||
import run.halo.app.infra.properties.HaloProperties;
|
||||
import run.halo.app.theme.ThemePathPolicy;
|
||||
|
||||
/**
|
||||
* Reconciler for theme.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class ThemeReconciler implements Reconciler {
|
||||
|
||||
private final ExtensionClient client;
|
||||
private final ThemePathPolicy themePathPolicy;
|
||||
|
||||
public ThemeReconciler(ExtensionClient client, HaloProperties haloProperties) {
|
||||
this.client = client;
|
||||
themePathPolicy = new ThemePathPolicy(haloProperties.getWorkDir());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result reconcile(Request request) {
|
||||
client.fetch(Theme.class, request.name())
|
||||
.ifPresent(theme -> {
|
||||
if (isDeleted(theme)) {
|
||||
reconcileThemeDeletion(theme);
|
||||
}
|
||||
});
|
||||
return new Result(false, null);
|
||||
}
|
||||
|
||||
private void reconcileThemeDeletion(Theme theme) {
|
||||
deleteThemeFiles(theme);
|
||||
// delete theme setting form
|
||||
String settingName = theme.getSpec().getSettingName();
|
||||
if (StringUtils.isNotBlank(settingName)) {
|
||||
client.fetch(Setting.class, settingName)
|
||||
.ifPresent(client::delete);
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteThemeFiles(Theme theme) {
|
||||
Path themeDir = themePathPolicy.generate(theme);
|
||||
try {
|
||||
FileSystemUtils.deleteRecursively(themeDir);
|
||||
} catch (IOException e) {
|
||||
throw new ThemeUninstallException("Failed to delete theme files.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isDeleted(Theme theme) {
|
||||
return theme.getMetadata().getDeletionTimestamp() != null;
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package run.halo.app.infra;
|
||||
package run.halo.app.infra.exception;
|
||||
|
||||
/**
|
||||
* Not found exception.
|
|
@ -1,4 +1,4 @@
|
|||
package run.halo.app.infra;
|
||||
package run.halo.app.infra.exception;
|
||||
|
||||
/**
|
||||
* @author guqing
|
|
@ -0,0 +1,16 @@
|
|||
package run.halo.app.infra.exception;
|
||||
|
||||
/**
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class ThemeUninstallException extends RuntimeException {
|
||||
|
||||
public ThemeUninstallException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public ThemeUninstallException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
|
@ -11,7 +11,7 @@ import org.thymeleaf.dialect.IDialect;
|
|||
import org.thymeleaf.spring6.ISpringWebFluxTemplateEngine;
|
||||
import org.thymeleaf.templateresolver.FileTemplateResolver;
|
||||
import org.thymeleaf.templateresolver.ITemplateResolver;
|
||||
import run.halo.app.infra.NotFoundException;
|
||||
import run.halo.app.infra.exception.NotFoundException;
|
||||
import run.halo.app.theme.engine.SpringWebFluxTemplateEngine;
|
||||
import run.halo.app.theme.message.ThemeMessageResolver;
|
||||
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
package run.halo.app.theme;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import org.springframework.util.Assert;
|
||||
import run.halo.app.core.extension.Theme;
|
||||
|
||||
/**
|
||||
* Policy for generating theme directory path.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class ThemePathPolicy {
|
||||
public static final String THEME_WORK_DIR = "themes";
|
||||
|
||||
private final Path workDir;
|
||||
|
||||
public ThemePathPolicy(Path workDir) {
|
||||
Assert.notNull(workDir, "The halo workDir must not be null.");
|
||||
this.workDir = workDir;
|
||||
}
|
||||
|
||||
public Path generate(Theme theme) {
|
||||
Assert.notNull(theme, "The theme must not be null.");
|
||||
String name = theme.getMetadata().getName();
|
||||
return workDir.resolve(THEME_WORK_DIR).resolve(name);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
package run.halo.app.core.extension.reconciler;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.util.FileSystemUtils;
|
||||
import org.springframework.util.ResourceUtils;
|
||||
import run.halo.app.core.extension.Setting;
|
||||
import run.halo.app.core.extension.Theme;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.extension.controller.Reconciler;
|
||||
import run.halo.app.infra.properties.HaloProperties;
|
||||
import run.halo.app.theme.ThemePathPolicy;
|
||||
|
||||
/**
|
||||
* Tests for {@link ThemeReconciler}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ThemeReconcilerTest {
|
||||
|
||||
@Mock
|
||||
private ExtensionClient extensionClient;
|
||||
|
||||
@Mock
|
||||
private HaloProperties haloProperties;
|
||||
|
||||
private File defaultTheme;
|
||||
|
||||
private Path tempDirectory;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws IOException {
|
||||
tempDirectory = Files.createTempDirectory("halo-theme-");
|
||||
defaultTheme = ResourceUtils.getFile("classpath:themes/default");
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() throws IOException {
|
||||
FileSystemUtils.deleteRecursively(tempDirectory);
|
||||
}
|
||||
|
||||
@Test
|
||||
void reconcileDelete() throws IOException {
|
||||
Path testWorkDir = tempDirectory.resolve("reconcile-delete");
|
||||
Files.createDirectory(testWorkDir);
|
||||
when(haloProperties.getWorkDir()).thenReturn(testWorkDir);
|
||||
|
||||
ThemeReconciler themeReconciler = new ThemeReconciler(extensionClient, haloProperties);
|
||||
ThemePathPolicy themePathPolicy = new ThemePathPolicy(testWorkDir);
|
||||
|
||||
Theme theme = new Theme();
|
||||
Metadata metadata = new Metadata();
|
||||
metadata.setName("theme-test");
|
||||
metadata.setDeletionTimestamp(Instant.now());
|
||||
theme.setMetadata(metadata);
|
||||
theme.setKind(Theme.KIND);
|
||||
theme.setApiVersion("theme.halo.run/v1alpha1");
|
||||
Theme.ThemeSpec themeSpec = new Theme.ThemeSpec();
|
||||
themeSpec.setSettingName("theme-test-setting");
|
||||
theme.setSpec(themeSpec);
|
||||
|
||||
Path defaultThemePath = themePathPolicy.generate(theme);
|
||||
|
||||
// copy to temp directory
|
||||
FileSystemUtils.copyRecursively(defaultTheme.toPath(), defaultThemePath);
|
||||
|
||||
assertThat(testWorkDir).isNotEmptyDirectory();
|
||||
assertThat(defaultThemePath).exists();
|
||||
|
||||
when(extensionClient.fetch(eq(Theme.class), eq(metadata.getName())))
|
||||
.thenReturn(Optional.of(theme));
|
||||
when(extensionClient.fetch(Setting.class, themeSpec.getSettingName()))
|
||||
.thenReturn(Optional.empty());
|
||||
|
||||
themeReconciler.reconcile(new Reconciler.Request(metadata.getName()));
|
||||
|
||||
verify(extensionClient, times(1)).fetch(eq(Theme.class), eq(metadata.getName()));
|
||||
verify(extensionClient, times(1)).fetch(eq(Setting.class), eq(themeSpec.getSettingName()));
|
||||
|
||||
assertThat(Files.exists(testWorkDir)).isTrue();
|
||||
assertThat(Files.exists(defaultThemePath)).isFalse();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue