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 0d479e39e..ac70a8a2b 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 @@ -2,9 +2,11 @@ package run.halo.app.core.extension.endpoint; import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder; import static org.springdoc.core.fn.builders.content.Builder.contentBuilder; +import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder; import static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuilder; import static org.springframework.web.reactive.function.server.RequestPredicates.contentType; +import io.swagger.v3.oas.annotations.enums.ParameterIn; import io.swagger.v3.oas.annotations.media.Schema; import java.io.IOException; import java.io.InputStream; @@ -17,6 +19,7 @@ import java.util.function.Predicate; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; import org.springdoc.core.fn.builders.schema.Builder; import org.springdoc.webflux.core.fn.SpringdocRouteBuilder; @@ -87,9 +90,48 @@ public class ThemeEndpoint implements CustomEndpoint { .response(responseBuilder() .implementation(Theme.class)) ) + .PUT("themes/{name}/reload-setting", this::reloadSetting, + builder -> builder.operationId("ReloadThemeSetting") + .description("Reload theme setting.") + .tag(tag) + .parameter(parameterBuilder() + .name("name") + .in(ParameterIn.PATH) + .required(true) + .implementation(String.class) + ) + .response(responseBuilder() + .implementation(Setting.class)) + ) .build(); } + Mono reloadSetting(ServerRequest request) { + String name = request.pathVariable("name"); + return client.fetch(Theme.class, name) + .filter(theme -> StringUtils.isNotBlank(theme.getSpec().getSettingName())) + .flatMap(theme -> { + String settingName = theme.getSpec().getSettingName(); + return ThemeUtils.loadThemeSetting(getThemePath(theme)) + .stream() + .filter(unstructured -> + settingName.equals(unstructured.getMetadata().getName())) + .findFirst() + .map(setting -> Unstructured.OBJECT_MAPPER.convertValue(setting, Setting.class)) + .map(setting -> client.fetch(Setting.class, settingName) + .flatMap(persistent -> { + // update spec to persisted setting + persistent.setSpec(setting.getSpec()); + return client.update(persistent); + }) + .switchIfEmpty(Mono.defer(() -> client.create(setting)))) + .orElse(Mono.empty()); + }) + .flatMap(setting -> ServerResponse.ok() + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(setting)); + } + public record InstallRequest( @Schema(required = true, description = "Theme zip file.") FilePart file) { } @@ -180,16 +222,19 @@ public class ThemeEndpoint implements CustomEndpoint { static class ThemeUtils { private static final String THEME_TMP_PREFIX = "halo-theme-"; private static final String[] themeManifests = {"theme.yaml", "theme.yml"}; - private static final String[] THEME_RESOURCES = { - "settings.yaml", - "settings.yml", - "config.yaml", - "config.yml" - }; - static List loadThemeResources(Path themePath) { + private static final String[] THEME_CONFIG = {"config.yaml", "config.yml"}; + + private static final String[] THEME_SETTING = {"settings.yaml", "settings.yml"}; + + static List loadThemeSetting(Path themePath) { + return loadUnstructured(themePath, THEME_SETTING); + } + + private static List loadUnstructured(Path themePath, + String[] themeSetting) { List resources = new ArrayList<>(4); - for (String themeResource : THEME_RESOURCES) { + for (String themeResource : themeSetting) { Path resourcePath = themePath.resolve(themeResource); if (Files.exists(resourcePath)) { resources.add(new FileSystemResource(resourcePath)); @@ -202,6 +247,11 @@ public class ThemeEndpoint implements CustomEndpoint { .load(); } + static List loadThemeResources(Path themePath) { + String[] resourceNames = ArrayUtils.addAll(THEME_SETTING, THEME_CONFIG); + return loadUnstructured(themePath, resourceNames); + } + static Unstructured unzipThemeTo(InputStream inputStream, Path themeWorkDir) { return unzipThemeTo(inputStream, themeWorkDir, false); } diff --git a/src/test/java/run/halo/app/core/extension/endpoint/ThemeEndpointTest.java b/src/test/java/run/halo/app/core/extension/endpoint/ThemeEndpointTest.java index 913e92d6a..83e15428b 100644 --- a/src/test/java/run/halo/app/core/extension/endpoint/ThemeEndpointTest.java +++ b/src/test/java/run/halo/app/core/extension/endpoint/ThemeEndpointTest.java @@ -10,12 +10,16 @@ import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.List; +import org.json.JSONException; 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.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.skyscreamer.jsonassert.JSONAssert; import org.springframework.core.io.FileSystemResource; import org.springframework.http.MediaType; import org.springframework.http.client.MultipartBodyBuilder; @@ -24,10 +28,13 @@ import org.springframework.util.FileSystemUtils; import org.springframework.util.ResourceUtils; import org.springframework.web.reactive.function.BodyInserters; import reactor.core.publisher.Mono; +import run.halo.app.core.extension.Setting; import run.halo.app.core.extension.Theme; +import run.halo.app.extension.Metadata; import run.halo.app.extension.ReactiveExtensionClient; import run.halo.app.extension.Unstructured; import run.halo.app.infra.properties.HaloProperties; +import run.halo.app.infra.utils.JsonUtils; import run.halo.app.infra.utils.YamlUnstructuredLoader; /** @@ -108,4 +115,84 @@ class ThemeEndpointTest { .expectStatus() .is5xxServerError(); } + + @Test + void reloadSetting() throws IOException { + Theme theme = new Theme(); + theme.setMetadata(new Metadata()); + theme.getMetadata().setName("fake-theme"); + theme.setSpec(new Theme.ThemeSpec()); + theme.getSpec().setSettingName("fake-setting"); + when(extensionClient.fetch(Theme.class, "fake-theme")) + .thenReturn(Mono.just(theme)); + Setting setting = new Setting(); + setting.setMetadata(new Metadata()); + setting.setSpec(new Setting.SettingSpec()); + setting.getSpec().setForms(List.of()); + when(extensionClient.fetch(Setting.class, "fake-setting")) + .thenReturn(Mono.just(setting)); + + when(haloProperties.getWorkDir()).thenReturn(tmpHaloWorkDir); + Path themeWorkDir = tmpHaloWorkDir.resolve("themes") + .resolve(theme.getMetadata().getName()); + if (!Files.exists(themeWorkDir)) { + Files.createDirectories(themeWorkDir); + } + Files.writeString(themeWorkDir.resolve("settings.yaml"), """ + apiVersion: v1alpha1 + kind: Setting + metadata: + name: fake-setting + spec: + forms: + - group: sns + label: 社交资料 + formSchema: + - $el: h1 + children: Register + """); + + when(extensionClient.update(any(Setting.class))) + .thenReturn(Mono.just(setting)); + ArgumentCaptor captor = ArgumentCaptor.forClass(Setting.class); + webTestClient.put() + .uri("/themes/fake-theme/reload-setting") + .exchange() + .expectStatus() + .isOk() + .expectBody(Setting.class) + .value(settingRes -> { + verify(extensionClient, times(1)).update(captor.capture()); + verify(extensionClient, times(0)).create(any(Setting.class)); + Setting value = captor.getValue(); + System.out.println(JsonUtils.objectToJson(value)); + try { + JSONAssert.assertEquals(""" + { + "spec": { + "forms": [ + { + "group": "sns", + "label": "社交资料", + "formSchema": [ + { + "$el": "h1", + "children": "Register" + } + ] + } + ] + }, + "apiVersion": "v1alpha1", + "kind": "Setting", + "metadata": {} + } + """, + JsonUtils.objectToJson(value), + true); + } catch (JSONException e) { + throw new RuntimeException(e); + } + }); + } } \ No newline at end of file