mirror of https://github.com/halo-dev/halo
feat: add an API to list uninstalled themes (#2586)
#### What type of PR is this? /kind feature /milestone 2.0 /area core /kind api-change #### What this PR does / why we need it: 新增 API 用于查询未安装的主题 #### Which issue(s) this PR fixes: Fixes #2554 #### Special notes for your reviewer: how to test it? 1. 安装几个主题 2. 直接解压几个主题到 work dir 的 themes 目录 3. 使用以下 endpoint 查询未安装的主题,期望获得所有未安装主题的 themes.yaml 信息 ``` /apis/api.console.halo.run/v1alpha1/themes?uninstalled=true ``` /cc @halo-dev/sig-halo #### Does this PR introduce a user-facing change? ```release-note 支持扫描主题目录下未安装的主题 ```pull/2592/head
parent
3d79484591
commit
58e98f0fc8
|
@ -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<String, String> queryParams) {
|
||||
super(queryParams);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Boolean getUninstalled() {
|
||||
return Boolean.parseBoolean(queryParams.getFirst("uninstalled"));
|
||||
}
|
||||
}
|
||||
|
||||
Mono<ServerResponse> listThemes(ServerRequest request) {
|
||||
MultiValueMap<String, String> 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<ListResult<Theme>> 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<Theme> subList = ListResult.subList(themes, page, size);
|
||||
return new ListResult<>(page, size, themes.size(), subList);
|
||||
});
|
||||
}
|
||||
|
||||
private Mono<List<Theme>> filterUnInstalledThemes(@NonNull List<Theme> 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<ServerResponse> 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<Theme> 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<Path> walkThemesFromPath(Path path) {
|
||||
return Flux.using(() -> Files.walk(path, 2),
|
||||
Flux::fromStream,
|
||||
BaseStream::close
|
||||
)
|
||||
.subscribeOn(Schedulers.boundedElastic());
|
||||
}
|
||||
|
||||
static List<Unstructured> 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;
|
||||
|
|
|
@ -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<T> implements Streamable<T> {
|
|||
public static <T> ListResult<T> emptyResult() {
|
||||
return new ListResult<>(List.of());
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually paginate the List collection.
|
||||
*/
|
||||
public static <T> List<T> subList(List<T> list, int page, int size) {
|
||||
if (page < 1) {
|
||||
return list;
|
||||
}
|
||||
List<T> 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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" ]
|
||||
|
||||
|
|
Loading…
Reference in New Issue