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 dfd7f48f8..9af62eb35 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 @@ -14,8 +14,10 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.function.Predicate; +import java.util.stream.BaseStream; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import lombok.extern.slf4j.Slf4j; @@ -28,6 +30,7 @@ import org.springframework.core.io.Resource; import org.springframework.http.MediaType; import org.springframework.http.codec.multipart.FilePart; import org.springframework.http.codec.multipart.Part; +import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.stereotype.Component; import org.springframework.util.Assert; @@ -46,8 +49,11 @@ import reactor.core.scheduler.Schedulers; import run.halo.app.core.extension.Setting; import run.halo.app.core.extension.Theme; import run.halo.app.extension.ConfigMap; +import run.halo.app.extension.ListResult; import run.halo.app.extension.ReactiveExtensionClient; import run.halo.app.extension.Unstructured; +import run.halo.app.extension.router.IListRequest; +import run.halo.app.extension.router.QueryParamBuildUtil; import run.halo.app.infra.exception.ThemeInstallationException; import run.halo.app.infra.properties.HaloProperties; import run.halo.app.infra.utils.DataBufferUtils; @@ -106,9 +112,65 @@ public class ThemeEndpoint implements CustomEndpoint { .response(responseBuilder() .implementation(Theme.class)) ) + .GET("themes", this::listThemes, + builder -> { + builder.operationId("ListThemes") + .description("List themes.") + .tag(tag) + .response(responseBuilder() + .implementation(ListResult.generateGenericClass(Theme.class))); + QueryParamBuildUtil.buildParametersFromType(builder, ThemeQuery.class); + } + ) .build(); } + public static class ThemeQuery extends IListRequest.QueryListRequest { + + public ThemeQuery(MultiValueMap queryParams) { + super(queryParams); + } + + @NonNull + public Boolean getUninstalled() { + return Boolean.parseBoolean(queryParams.getFirst("uninstalled")); + } + } + + Mono listThemes(ServerRequest request) { + MultiValueMap queryParams = request.queryParams(); + ThemeQuery query = new ThemeQuery(queryParams); + return Mono.defer(() -> { + if (query.getUninstalled()) { + return listUninstalled(query); + } + return client.list(Theme.class, null, null, query.getPage(), query.getSize()); + }).flatMap(extensions -> ServerResponse.ok().bodyValue(extensions)); + } + + Mono> listUninstalled(ThemeQuery query) { + Path path = themePathPolicy.themesDir(); + return ThemeUtils.listAllThemesFromThemeDir(path) + .collectList() + .flatMap(this::filterUnInstalledThemes) + .map(themes -> { + Integer page = query.getPage(); + Integer size = query.getSize(); + List subList = ListResult.subList(themes, page, size); + return new ListResult<>(page, size, themes.size(), subList); + }); + } + + private Mono> filterUnInstalledThemes(@NonNull List allThemes) { + return client.list(Theme.class, null, null) + .map(theme -> theme.getMetadata().getName()) + .collectList() + .map(installed -> allThemes.stream() + .filter(theme -> !installed.contains(theme.getMetadata().getName())) + .toList() + ); + } + Mono reloadSetting(ServerRequest request) { String name = request.pathVariable("name"); return client.fetch(Theme.class, name) @@ -238,12 +300,29 @@ 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"}; + public static final String[] THEME_MANIFESTS = {"theme.yaml", "theme.yml"}; private static final String[] THEME_CONFIG = {"config.yaml", "config.yml"}; private static final String[] THEME_SETTING = {"settings.yaml", "settings.yml"}; + static Flux listAllThemesFromThemeDir(Path themesDir) { + return walkThemesFromPath(themesDir) + .filter(Files::isDirectory) + .map(themePath -> loadUnstructured(themePath, THEME_MANIFESTS)) + .map(unstructured -> Unstructured.OBJECT_MAPPER.convertValue(unstructured, + Theme.class)) + .sort(Comparator.comparing(theme -> theme.getMetadata().getName())); + } + + private static Flux walkThemesFromPath(Path path) { + return Flux.using(() -> Files.walk(path, 2), + Flux::fromStream, + BaseStream::close + ) + .subscribeOn(Schedulers.boundedElastic()); + } + static List loadThemeSetting(Path themePath) { return loadUnstructured(themePath, THEME_SETTING); } @@ -329,7 +408,7 @@ public class ThemeEndpoint implements CustomEndpoint { @Nullable private static Path resolveThemeManifest(Path tempDirectory) { - for (String themeManifest : themeManifests) { + for (String themeManifest : THEME_MANIFESTS) { Path path = tempDirectory.resolve(themeManifest); if (Files.exists(path)) { return path; diff --git a/src/main/java/run/halo/app/extension/ListResult.java b/src/main/java/run/halo/app/extension/ListResult.java index 06ae08a7c..605577eaa 100644 --- a/src/main/java/run/halo/app/extension/ListResult.java +++ b/src/main/java/run/halo/app/extension/ListResult.java @@ -3,6 +3,7 @@ package run.halo.app.extension; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; +import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; @@ -128,4 +129,21 @@ public class ListResult implements Streamable { public static ListResult emptyResult() { return new ListResult<>(List.of()); } + + /** + * Manually paginate the List collection. + */ + public static List subList(List list, int page, int size) { + if (page < 1) { + return list; + } + List listSort = new ArrayList<>(); + int total = list.size(); + int pageStart = page == 1 ? 0 : (page - 1) * size; + int pageEnd = Math.min(total, page * size); + if (total > pageStart) { + listSort = list.subList(pageStart, pageEnd); + } + return listSort; + } } diff --git a/src/main/java/run/halo/app/theme/ThemePathPolicy.java b/src/main/java/run/halo/app/theme/ThemePathPolicy.java index c1ffaa316..a7d15fd83 100644 --- a/src/main/java/run/halo/app/theme/ThemePathPolicy.java +++ b/src/main/java/run/halo/app/theme/ThemePathPolicy.java @@ -23,6 +23,10 @@ public class ThemePathPolicy { 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); + return themesDir().resolve(name); + } + + public Path themesDir() { + return workDir.resolve(ThemePathPolicy.THEME_WORK_DIR); } } diff --git a/src/main/resources/extensions/role-template-theme.yaml b/src/main/resources/extensions/role-template-theme.yaml index fe8852e9a..eec831780 100644 --- a/src/main/resources/extensions/role-template-theme.yaml +++ b/src/main/resources/extensions/role-template-theme.yaml @@ -18,7 +18,7 @@ rules: resources: [ "themes", "themes/reload-setting" ] verbs: [ "*" ] - nonResourceURLs: [ "/apis/api.console.halo.run/themes/install" ] - verbs: [ "post" ] + verbs: [ "create" ] --- apiVersion: v1alpha1 kind: "Role" @@ -36,5 +36,6 @@ rules: resources: [ "themes" ] verbs: [ "get", "list" ] - apiGroups: [ "api.console.halo.run" ] - resources: [ "singlepages" ] + resources: [ "themes" ] verbs: [ "get", "list" ] +