Skip to content

Commit

Permalink
feat: add an API to list uninstalled themes (#2586)
Browse files Browse the repository at this point in the history
#### 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
支持扫描主题目录下未安装的主题
```
  • Loading branch information
guqing authored Oct 18, 2022
1 parent 3d79484 commit 58e98f0
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@
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;
Expand All @@ -28,6 +30,7 @@
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;
Expand All @@ -46,8 +49,11 @@
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;
Expand Down Expand Up @@ -106,9 +112,65 @@ public RouterFunction<ServerResponse> endpoint() {
.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)
Expand Down Expand Up @@ -238,12 +300,29 @@ private Predicate<Unstructured> hasConfigYaml(Theme theme) {

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);
}
Expand Down Expand Up @@ -329,7 +408,7 @@ static Unstructured loadThemeManifest(Path themeManifestPath) {

@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;
Expand Down
18 changes: 18 additions & 0 deletions src/main/java/run/halo/app/extension/ListResult.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
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;
Expand Down Expand Up @@ -128,4 +129,21 @@ public static <T> Class<?> generateGenericClass(Class<T> type) {
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;
}
}
6 changes: 5 additions & 1 deletion src/main/java/run/halo/app/theme/ThemePathPolicy.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ public ThemePathPolicy(Path 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);
return themesDir().resolve(name);
}

public Path themesDir() {
return workDir.resolve(ThemePathPolicy.THEME_WORK_DIR);
}
}
5 changes: 3 additions & 2 deletions src/main/resources/extensions/role-template-theme.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -36,5 +36,6 @@ rules:
resources: [ "themes" ]
verbs: [ "get", "list" ]
- apiGroups: [ "api.console.halo.run" ]
resources: [ "singlepages" ]
resources: [ "themes" ]
verbs: [ "get", "list" ]

0 comments on commit 58e98f0

Please sign in to comment.