From 7026681747db43125369db6bb8121b5910915171 Mon Sep 17 00:00:00 2001 From: guqing <38999863+guqing@users.noreply.github.com> Date: Wed, 10 Aug 2022 17:12:21 +0800 Subject: [PATCH] feat: add theme uninstall endpoint (#2315) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### What type of PR is this? /kind feature /milestone 2.0 /area core /kind api-change #### What this PR does / why we need it: 新增主题卸载 endpoint #### Which issue(s) this PR fixes: Fixes #2306 #### Special notes for your reviewer: /cc @halo-dev/sig-halo #### Does this PR introduce a user-facing change? ```release-note None ``` --- .../app/config/ExtensionConfiguration.java | 11 ++ .../extension/endpoint/ThemeEndpoint.java | 2 +- .../extension/reconciler/ThemeReconciler.java | 64 +++++++++++ .../{ => exception}/NotFoundException.java | 2 +- .../ThemeInstallationException.java | 2 +- .../exception/ThemeUninstallException.java | 16 +++ .../halo/app/theme/TemplateEngineManager.java | 2 +- .../run/halo/app/theme/ThemePathPolicy.java | 28 +++++ .../reconciler/ThemeReconcilerTest.java | 102 ++++++++++++++++++ 9 files changed, 225 insertions(+), 4 deletions(-) create mode 100644 src/main/java/run/halo/app/core/extension/reconciler/ThemeReconciler.java rename src/main/java/run/halo/app/infra/{ => exception}/NotFoundException.java (89%) rename src/main/java/run/halo/app/infra/{ => exception}/ThemeInstallationException.java (89%) create mode 100644 src/main/java/run/halo/app/infra/exception/ThemeUninstallException.java create mode 100644 src/main/java/run/halo/app/theme/ThemePathPolicy.java create mode 100644 src/test/java/run/halo/app/core/extension/reconciler/ThemeReconcilerTest.java diff --git a/src/main/java/run/halo/app/config/ExtensionConfiguration.java b/src/main/java/run/halo/app/config/ExtensionConfiguration.java index d7dd4320c..02efb873f 100644 --- a/src/main/java/run/halo/app/config/ExtensionConfiguration.java +++ b/src/main/java/run/halo/app/config/ExtensionConfiguration.java @@ -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(); + } } } diff --git a/src/main/java/run/halo/app/core/extension/endpoint/ThemeEndpoint.java b/src/main/java/run/halo/app/core/extension/endpoint/ThemeEndpoint.java index 363779cdd..d298d91f0 100644 --- a/src/main/java/run/halo/app/core/extension/endpoint/ThemeEndpoint.java +++ b/src/main/java/run/halo/app/core/extension/endpoint/ThemeEndpoint.java @@ -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; diff --git a/src/main/java/run/halo/app/core/extension/reconciler/ThemeReconciler.java b/src/main/java/run/halo/app/core/extension/reconciler/ThemeReconciler.java new file mode 100644 index 000000000..a883b50a2 --- /dev/null +++ b/src/main/java/run/halo/app/core/extension/reconciler/ThemeReconciler.java @@ -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; + } +} diff --git a/src/main/java/run/halo/app/infra/NotFoundException.java b/src/main/java/run/halo/app/infra/exception/NotFoundException.java similarity index 89% rename from src/main/java/run/halo/app/infra/NotFoundException.java rename to src/main/java/run/halo/app/infra/exception/NotFoundException.java index db22f108d..3fcad82db 100644 --- a/src/main/java/run/halo/app/infra/NotFoundException.java +++ b/src/main/java/run/halo/app/infra/exception/NotFoundException.java @@ -1,4 +1,4 @@ -package run.halo.app.infra; +package run.halo.app.infra.exception; /** * Not found exception. diff --git a/src/main/java/run/halo/app/infra/ThemeInstallationException.java b/src/main/java/run/halo/app/infra/exception/ThemeInstallationException.java similarity index 89% rename from src/main/java/run/halo/app/infra/ThemeInstallationException.java rename to src/main/java/run/halo/app/infra/exception/ThemeInstallationException.java index bf9dc4151..0f639d103 100644 --- a/src/main/java/run/halo/app/infra/ThemeInstallationException.java +++ b/src/main/java/run/halo/app/infra/exception/ThemeInstallationException.java @@ -1,4 +1,4 @@ -package run.halo.app.infra; +package run.halo.app.infra.exception; /** * @author guqing diff --git a/src/main/java/run/halo/app/infra/exception/ThemeUninstallException.java b/src/main/java/run/halo/app/infra/exception/ThemeUninstallException.java new file mode 100644 index 000000000..fd337bab2 --- /dev/null +++ b/src/main/java/run/halo/app/infra/exception/ThemeUninstallException.java @@ -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); + } +} diff --git a/src/main/java/run/halo/app/theme/TemplateEngineManager.java b/src/main/java/run/halo/app/theme/TemplateEngineManager.java index 87e40114b..f61812571 100644 --- a/src/main/java/run/halo/app/theme/TemplateEngineManager.java +++ b/src/main/java/run/halo/app/theme/TemplateEngineManager.java @@ -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; diff --git a/src/main/java/run/halo/app/theme/ThemePathPolicy.java b/src/main/java/run/halo/app/theme/ThemePathPolicy.java new file mode 100644 index 000000000..c1ffaa316 --- /dev/null +++ b/src/main/java/run/halo/app/theme/ThemePathPolicy.java @@ -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); + } +} diff --git a/src/test/java/run/halo/app/core/extension/reconciler/ThemeReconcilerTest.java b/src/test/java/run/halo/app/core/extension/reconciler/ThemeReconcilerTest.java new file mode 100644 index 000000000..68e186a6d --- /dev/null +++ b/src/test/java/run/halo/app/core/extension/reconciler/ThemeReconcilerTest.java @@ -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(); + } +} \ No newline at end of file