mirror of https://github.com/halo-dev/halo
feat: add the ability to install themes remotely via URI (#3939)
#### What type of PR is this? /kind improvement /area core /milestone 2.6.x /kind api-change #### What this PR does / why we need it: 支持通过 URI 远程安装和升级主题 how to test it? 1. 测试主题安装 ```shell curl -u admin:admin -X POST http://localhost:8090/apis/api.console.halo.run/v1alpha1/themes/-/install-from-uri --data '{ "uri": "https://halo.run/apis/api.store.halo.run/v1alpha1/applications/app-eiTyL/releases/app-release-QSyjc/download/app-release-QSyjc-JOSOB" }' ``` 2. 测试主题升级 ```shell curl -u admin:admin -X POST http://localhost:8090/apis/api.console.halo.run/v1alpha1/themes/guqing-higan/upgrade-from-uri --data '{ "uri": "https://halo.run/apis/api.store.halo.run/v1alpha1/applications/app-eiTyL/releases/app-release-QSyjc/download/app-release-QSyjc-JOSOB" }' ``` #### Which issue(s) this PR fixes: Fixes #2291 #### Does this PR introduce a user-facing change? ```release-note 支持通过 URI 远程安装和升级主题 ``` --------- Co-authored-by: Ryan Wang <i@ryanc.cc>pull/3973/head
parent
8deea08231
commit
170cf4e412
|
@ -1,5 +1,6 @@
|
||||||
package run.halo.app.core.extension.theme;
|
package run.halo.app.core.extension.theme;
|
||||||
|
|
||||||
|
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
|
||||||
import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;
|
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.content.Builder.contentBuilder;
|
||||||
import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;
|
import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;
|
||||||
|
@ -11,6 +12,7 @@ import static run.halo.app.infra.utils.DataBufferUtils.toInputStream;
|
||||||
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
@ -43,10 +45,14 @@ import run.halo.app.extension.ListResult;
|
||||||
import run.halo.app.extension.ReactiveExtensionClient;
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
import run.halo.app.extension.router.IListRequest;
|
import run.halo.app.extension.router.IListRequest;
|
||||||
import run.halo.app.extension.router.QueryParamBuildUtil;
|
import run.halo.app.extension.router.QueryParamBuildUtil;
|
||||||
|
import run.halo.app.infra.ReactiveUrlDataBufferFetcher;
|
||||||
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
||||||
import run.halo.app.infra.SystemSetting;
|
import run.halo.app.infra.SystemSetting;
|
||||||
import run.halo.app.infra.ThemeRootGetter;
|
import run.halo.app.infra.ThemeRootGetter;
|
||||||
import run.halo.app.infra.exception.NotFoundException;
|
import run.halo.app.infra.exception.NotFoundException;
|
||||||
|
import run.halo.app.infra.exception.ThemeInstallationException;
|
||||||
|
import run.halo.app.infra.exception.ThemeUpgradeException;
|
||||||
|
import run.halo.app.infra.utils.DataBufferUtils;
|
||||||
import run.halo.app.infra.utils.JsonUtils;
|
import run.halo.app.infra.utils.JsonUtils;
|
||||||
import run.halo.app.theme.TemplateEngineManager;
|
import run.halo.app.theme.TemplateEngineManager;
|
||||||
|
|
||||||
|
@ -71,6 +77,8 @@ public class ThemeEndpoint implements CustomEndpoint {
|
||||||
|
|
||||||
private final SystemConfigurableEnvironmentFetcher systemEnvironmentFetcher;
|
private final SystemConfigurableEnvironmentFetcher systemEnvironmentFetcher;
|
||||||
|
|
||||||
|
private final ReactiveUrlDataBufferFetcher reactiveUrlDataBufferFetcher;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public RouterFunction<ServerResponse> endpoint() {
|
public RouterFunction<ServerResponse> endpoint() {
|
||||||
final var tag = "api.console.halo.run/v1alpha1/Theme";
|
final var tag = "api.console.halo.run/v1alpha1/Theme";
|
||||||
|
@ -89,6 +97,39 @@ public class ThemeEndpoint implements CustomEndpoint {
|
||||||
.response(responseBuilder()
|
.response(responseBuilder()
|
||||||
.implementation(Theme.class))
|
.implementation(Theme.class))
|
||||||
)
|
)
|
||||||
|
.POST("themes/-/install-from-uri", this::installFromUri,
|
||||||
|
builder -> builder.operationId("InstallThemeFromUri")
|
||||||
|
.description("Install a theme from uri.")
|
||||||
|
.tag(tag)
|
||||||
|
.requestBody(requestBodyBuilder()
|
||||||
|
.required(true)
|
||||||
|
.content(contentBuilder()
|
||||||
|
.mediaType(MediaType.APPLICATION_JSON_VALUE)
|
||||||
|
.schema(schemaBuilder()
|
||||||
|
.implementation(InstallFromUriRequest.class))
|
||||||
|
))
|
||||||
|
.response(responseBuilder()
|
||||||
|
.implementation(Theme.class))
|
||||||
|
)
|
||||||
|
.POST("themes/{name}/upgrade-from-uri", this::upgradeFromUri,
|
||||||
|
builder -> builder.operationId("UpgradeThemeFromUri")
|
||||||
|
.description("Upgrade a theme from uri.")
|
||||||
|
.tag(tag)
|
||||||
|
.parameter(parameterBuilder()
|
||||||
|
.in(ParameterIn.PATH)
|
||||||
|
.name("name")
|
||||||
|
.required(true)
|
||||||
|
)
|
||||||
|
.requestBody(requestBodyBuilder()
|
||||||
|
.required(true)
|
||||||
|
.content(contentBuilder()
|
||||||
|
.mediaType(MediaType.APPLICATION_JSON_VALUE)
|
||||||
|
.schema(schemaBuilder()
|
||||||
|
.implementation(UpgradeFromUriRequest.class))
|
||||||
|
))
|
||||||
|
.response(responseBuilder()
|
||||||
|
.implementation(Theme.class))
|
||||||
|
)
|
||||||
.POST("themes/{name}/upgrade", this::upgrade,
|
.POST("themes/{name}/upgrade", this::upgrade,
|
||||||
builder -> builder.operationId("UpgradeTheme")
|
builder -> builder.operationId("UpgradeTheme")
|
||||||
.description("Upgrade theme")
|
.description("Upgrade theme")
|
||||||
|
@ -200,6 +241,45 @@ public class ThemeEndpoint implements CustomEndpoint {
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Mono<ServerResponse> upgradeFromUri(ServerRequest request) {
|
||||||
|
final var name = request.pathVariable("name");
|
||||||
|
return request.bodyToMono(UpgradeFromUriRequest.class)
|
||||||
|
.flatMap(upgradeRequest -> Mono.fromCallable(() -> DataBufferUtils.toInputStream(
|
||||||
|
reactiveUrlDataBufferFetcher.fetch(upgradeRequest.uri())))
|
||||||
|
)
|
||||||
|
.doOnError(throwable -> {
|
||||||
|
log.error("Failed to fetch zip file from uri.", throwable);
|
||||||
|
throw new ThemeUpgradeException("Failed to fetch zip file from uri.", null,
|
||||||
|
null);
|
||||||
|
})
|
||||||
|
.flatMap(inputStream -> themeService.upgrade(name, inputStream))
|
||||||
|
.flatMap((updatedTheme) -> templateEngineManager.clearCache(
|
||||||
|
updatedTheme.getMetadata().getName())
|
||||||
|
.thenReturn(updatedTheme)
|
||||||
|
)
|
||||||
|
.flatMap(theme -> ServerResponse.ok()
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.bodyValue(theme)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mono<ServerResponse> installFromUri(ServerRequest request) {
|
||||||
|
return request.bodyToMono(InstallFromUriRequest.class)
|
||||||
|
.flatMap(installRequest -> Mono.fromCallable(() -> DataBufferUtils.toInputStream(
|
||||||
|
reactiveUrlDataBufferFetcher.fetch(installRequest.uri())))
|
||||||
|
)
|
||||||
|
.doOnError(throwable -> {
|
||||||
|
log.error("Failed to fetch zip file from uri.", throwable);
|
||||||
|
throw new ThemeInstallationException("Failed to fetch zip file from uri.", null,
|
||||||
|
null);
|
||||||
|
})
|
||||||
|
.flatMap(themeService::install)
|
||||||
|
.flatMap(theme -> ServerResponse.ok()
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.bodyValue(theme)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private Mono<ServerResponse> activateTheme(ServerRequest request) {
|
private Mono<ServerResponse> activateTheme(ServerRequest request) {
|
||||||
final var activatedThemeName = request.pathVariable("name");
|
final var activatedThemeName = request.pathVariable("name");
|
||||||
return client.fetch(Theme.class, activatedThemeName)
|
return client.fetch(Theme.class, activatedThemeName)
|
||||||
|
@ -332,6 +412,9 @@ public class ThemeEndpoint implements CustomEndpoint {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public record UpgradeFromUriRequest(@Schema(requiredMode = REQUIRED) URI uri) {
|
||||||
|
}
|
||||||
|
|
||||||
public static class UpgradeRequest implements IUpgradeRequest {
|
public static class UpgradeRequest implements IUpgradeRequest {
|
||||||
|
|
||||||
private final MultiValueMap<String, Part> multipartData;
|
private final MultiValueMap<String, Part> multipartData;
|
||||||
|
@ -418,6 +501,9 @@ public class ThemeEndpoint implements CustomEndpoint {
|
||||||
@Schema(required = true, description = "Theme zip file.") FilePart file) {
|
@Schema(required = true, description = "Theme zip file.") FilePart file) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public record InstallFromUriRequest(@Schema(requiredMode = REQUIRED) URI uri) {
|
||||||
|
}
|
||||||
|
|
||||||
Mono<ServerResponse> install(ServerRequest request) {
|
Mono<ServerResponse> install(ServerRequest request) {
|
||||||
return request.body(BodyExtractors.toMultipartData())
|
return request.body(BodyExtractors.toMultipartData())
|
||||||
.flatMap(this::getZipFilePart)
|
.flatMap(this::getZipFilePart)
|
||||||
|
|
|
@ -19,20 +19,25 @@ import java.util.concurrent.atomic.AtomicReference;
|
||||||
import java.util.stream.BaseStream;
|
import java.util.stream.BaseStream;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
import java.util.zip.ZipInputStream;
|
import java.util.zip.ZipInputStream;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.core.io.FileSystemResource;
|
import org.springframework.core.io.FileSystemResource;
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
import org.springframework.lang.Nullable;
|
import org.springframework.lang.Nullable;
|
||||||
import org.springframework.util.CollectionUtils;
|
import org.springframework.util.CollectionUtils;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
import org.springframework.web.server.ServerWebInputException;
|
||||||
import reactor.core.Exceptions;
|
import reactor.core.Exceptions;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import reactor.core.scheduler.Schedulers;
|
import reactor.core.scheduler.Schedulers;
|
||||||
import run.halo.app.core.extension.Theme;
|
import run.halo.app.core.extension.Theme;
|
||||||
import run.halo.app.extension.Unstructured;
|
import run.halo.app.extension.Unstructured;
|
||||||
|
import run.halo.app.infra.exception.ThemeAlreadyExistsException;
|
||||||
import run.halo.app.infra.exception.ThemeInstallationException;
|
import run.halo.app.infra.exception.ThemeInstallationException;
|
||||||
import run.halo.app.infra.utils.FileUtils;
|
import run.halo.app.infra.utils.FileUtils;
|
||||||
import run.halo.app.infra.utils.YamlUnstructuredLoader;
|
import run.halo.app.infra.utils.YamlUnstructuredLoader;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
class ThemeUtils {
|
class ThemeUtils {
|
||||||
private static final String THEME_TMP_PREFIX = "halo-theme-";
|
private static final String THEME_TMP_PREFIX = "halo-theme-";
|
||||||
private static final String[] THEME_MANIFESTS = {"theme.yaml", "theme.yml"};
|
private static final String[] THEME_MANIFESTS = {"theme.yaml", "theme.yml"};
|
||||||
|
@ -97,7 +102,11 @@ class ThemeUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
static Mono<Unstructured> unzipThemeTo(InputStream inputStream, Path themeWorkDir) {
|
static Mono<Unstructured> unzipThemeTo(InputStream inputStream, Path themeWorkDir) {
|
||||||
return unzipThemeTo(inputStream, themeWorkDir, false);
|
return unzipThemeTo(inputStream, themeWorkDir, false)
|
||||||
|
.onErrorMap(e -> !(e instanceof ResponseStatusException), e -> {
|
||||||
|
log.error("Failed to unzip theme", e);
|
||||||
|
throw new ServerWebInputException("Failed to unzip theme");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static Mono<Unstructured> unzipThemeTo(InputStream inputStream, Path themeWorkDir,
|
static Mono<Unstructured> unzipThemeTo(InputStream inputStream, Path themeWorkDir,
|
||||||
|
@ -130,8 +139,7 @@ class ThemeUtils {
|
||||||
var themeTargetPath = themeWorkDir.resolve(themeName);
|
var themeTargetPath = themeWorkDir.resolve(themeName);
|
||||||
try {
|
try {
|
||||||
if (!override && !FileUtils.isEmpty(themeTargetPath)) {
|
if (!override && !FileUtils.isEmpty(themeTargetPath)) {
|
||||||
throw new ThemeInstallationException("Theme already exists.",
|
throw new ThemeAlreadyExistsException(themeName);
|
||||||
"problemDetail.theme.install.alreadyExists", new Object[] {themeName});
|
|
||||||
}
|
}
|
||||||
// install theme to theme work dir
|
// install theme to theme work dir
|
||||||
copyRecursively(themeManifestPath.getParent(), themeTargetPath);
|
copyRecursively(themeManifestPath.getParent(), themeTargetPath);
|
||||||
|
@ -141,7 +149,10 @@ class ThemeUtils {
|
||||||
throw Exceptions.propagate(e);
|
throw Exceptions.propagate(e);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.doFinally(signalType -> deleteRecursivelyAndSilently(tempDir.get()));
|
.doFinally(signalType -> {
|
||||||
|
FileUtils.closeQuietly(inputStream);
|
||||||
|
deleteRecursivelyAndSilently(tempDir.get());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static Unstructured loadThemeManifest(Path themeManifestPath) {
|
static Unstructured loadThemeManifest(Path themeManifestPath) {
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
package run.halo.app.infra;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import org.springframework.core.io.buffer.DataBuffer;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.reactive.function.client.WebClient;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.netty.http.client.HttpClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>A default implementation of {@link ReactiveUrlDataBufferFetcher}.</p>
|
||||||
|
*
|
||||||
|
* @author guqing
|
||||||
|
* @since 2.6.0
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class DefaultReactiveUrlDataBufferFetcher implements ReactiveUrlDataBufferFetcher {
|
||||||
|
private final HttpClient httpClient = HttpClient.create()
|
||||||
|
.followRedirect(true);
|
||||||
|
private final WebClient webClient = WebClient.builder()
|
||||||
|
.clientConnector(new ReactorClientHttpConnector(httpClient))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Flux<DataBuffer> fetch(URI uri) {
|
||||||
|
return webClient.get()
|
||||||
|
.uri(uri)
|
||||||
|
.accept(MediaType.APPLICATION_OCTET_STREAM)
|
||||||
|
.retrieve()
|
||||||
|
.bodyToFlux(DataBuffer.class);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
package run.halo.app.infra;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import org.springframework.core.io.buffer.DataBuffer;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>{@link DataBuffer} stream fetcher from uri.</p>
|
||||||
|
*
|
||||||
|
* @author guqing
|
||||||
|
* @since 2.6.0
|
||||||
|
*/
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface ReactiveUrlDataBufferFetcher {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Fetch data buffer flux from uri.</p>
|
||||||
|
*
|
||||||
|
* @param uri uri to fetch
|
||||||
|
* @return data buffer flux
|
||||||
|
*/
|
||||||
|
Flux<DataBuffer> fetch(URI uri);
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
package run.halo.app.infra.exception;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import org.springframework.lang.NonNull;
|
||||||
|
import org.springframework.web.server.ServerWebInputException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link ThemeAlreadyExistsException} indicates the provided theme has already installed before.
|
||||||
|
*
|
||||||
|
* @author guqing
|
||||||
|
* @since 2.6.0
|
||||||
|
*/
|
||||||
|
public class ThemeAlreadyExistsException extends ServerWebInputException {
|
||||||
|
|
||||||
|
public static final String THEME_ALREADY_EXISTS_TYPE =
|
||||||
|
"https://halo.run/probs/theme-alreay-exists";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a {@code ThemeAlreadyExistsException} with the given theme name.
|
||||||
|
*
|
||||||
|
* @param themeName theme name must not be blank
|
||||||
|
*/
|
||||||
|
public ThemeAlreadyExistsException(@NonNull String themeName) {
|
||||||
|
super("Theme already exists.", null, null, "problemDetail.theme.install.alreadyExists",
|
||||||
|
new Object[] {themeName});
|
||||||
|
setType(URI.create(THEME_ALREADY_EXISTS_TYPE));
|
||||||
|
getBody().setProperty("themeName", themeName);
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,7 +15,8 @@ rules:
|
||||||
resources: [ "themes" ]
|
resources: [ "themes" ]
|
||||||
verbs: [ "*" ]
|
verbs: [ "*" ]
|
||||||
- apiGroups: [ "api.console.halo.run" ]
|
- apiGroups: [ "api.console.halo.run" ]
|
||||||
resources: [ "themes", "themes/reload", "themes/resetconfig", "themes/config", "themes/activation" ]
|
resources: [ "themes", "themes/reload", "themes/resetconfig", "themes/config", "themes/activation",
|
||||||
|
"themes/install-from-uri", "themes/upgrade-from-uri" ]
|
||||||
verbs: [ "*" ]
|
verbs: [ "*" ]
|
||||||
- nonResourceURLs: [ "/apis/api.console.halo.run/themes/install" ]
|
- nonResourceURLs: [ "/apis/api.console.halo.run/themes/install" ]
|
||||||
verbs: [ "create" ]
|
verbs: [ "create" ]
|
||||||
|
|
|
@ -4,6 +4,7 @@ import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
import static org.mockito.ArgumentMatchers.isA;
|
import static org.mockito.ArgumentMatchers.isA;
|
||||||
import static org.mockito.Mockito.lenient;
|
import static org.mockito.Mockito.lenient;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.times;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
@ -12,6 +13,7 @@ import static org.springframework.web.reactive.function.BodyInserters.fromMultip
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.net.URI;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
@ -23,18 +25,21 @@ import org.mockito.InjectMocks;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
import org.springframework.core.io.FileSystemResource;
|
import org.springframework.core.io.FileSystemResource;
|
||||||
|
import org.springframework.core.io.buffer.DataBuffer;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.client.MultipartBodyBuilder;
|
import org.springframework.http.client.MultipartBodyBuilder;
|
||||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||||
import org.springframework.util.FileSystemUtils;
|
import org.springframework.util.FileSystemUtils;
|
||||||
import org.springframework.util.ResourceUtils;
|
import org.springframework.util.ResourceUtils;
|
||||||
import org.springframework.web.server.ServerWebInputException;
|
import org.springframework.web.server.ServerWebInputException;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.core.extension.Setting;
|
import run.halo.app.core.extension.Setting;
|
||||||
import run.halo.app.core.extension.Theme;
|
import run.halo.app.core.extension.Theme;
|
||||||
import run.halo.app.extension.ConfigMap;
|
import run.halo.app.extension.ConfigMap;
|
||||||
import run.halo.app.extension.Metadata;
|
import run.halo.app.extension.Metadata;
|
||||||
import run.halo.app.extension.ReactiveExtensionClient;
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
|
import run.halo.app.infra.ReactiveUrlDataBufferFetcher;
|
||||||
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
||||||
import run.halo.app.infra.SystemSetting;
|
import run.halo.app.infra.SystemSetting;
|
||||||
import run.halo.app.infra.ThemeRootGetter;
|
import run.halo.app.infra.ThemeRootGetter;
|
||||||
|
@ -64,6 +69,9 @@ class ThemeEndpointTest {
|
||||||
@Mock
|
@Mock
|
||||||
private SystemConfigurableEnvironmentFetcher environmentFetcher;
|
private SystemConfigurableEnvironmentFetcher environmentFetcher;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ReactiveUrlDataBufferFetcher reactiveUrlDataBufferFetcher;
|
||||||
|
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
ThemeEndpoint themeEndpoint;
|
ThemeEndpoint themeEndpoint;
|
||||||
|
|
||||||
|
@ -138,6 +146,32 @@ class ThemeEndpointTest {
|
||||||
verify(templateEngineManager, times(1)).clearCache(eq("default"));
|
verify(templateEngineManager, times(1)).clearCache(eq("default"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void upgradeFromUri() {
|
||||||
|
final URI uri = URI.create("https://example.com/test-theme.zip");
|
||||||
|
Theme fakeTheme = mock(Theme.class);
|
||||||
|
Metadata metadata = new Metadata();
|
||||||
|
metadata.setName("default");
|
||||||
|
when(fakeTheme.getMetadata()).thenReturn(metadata);
|
||||||
|
when(themeService.upgrade(eq("default"), isA(InputStream.class)))
|
||||||
|
.thenReturn(Mono.just(fakeTheme));
|
||||||
|
when(reactiveUrlDataBufferFetcher.fetch(eq(uri)))
|
||||||
|
.thenReturn(Flux.just(mock(DataBuffer.class)));
|
||||||
|
when(templateEngineManager.clearCache(eq("default")))
|
||||||
|
.thenReturn(Mono.empty());
|
||||||
|
var body = new ThemeEndpoint.UpgradeFromUriRequest(uri);
|
||||||
|
webTestClient.post()
|
||||||
|
.uri("/themes/default/upgrade-from-uri")
|
||||||
|
.bodyValue(body)
|
||||||
|
.exchange()
|
||||||
|
.expectStatus().isOk();
|
||||||
|
|
||||||
|
verify(themeService).upgrade(eq("default"), isA(InputStream.class));
|
||||||
|
|
||||||
|
verify(templateEngineManager, times(1)).clearCache(eq("default"));
|
||||||
|
|
||||||
|
verify(reactiveUrlDataBufferFetcher).fetch(eq(uri));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -173,6 +207,25 @@ class ThemeEndpointTest {
|
||||||
.expectStatus().is5xxServerError();
|
.expectStatus().is5xxServerError();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void installFromUri() {
|
||||||
|
final URI uri = URI.create("https://example.com/test-theme.zip");
|
||||||
|
Theme fakeTheme = mock(Theme.class);
|
||||||
|
when(themeService.install(isA(InputStream.class)))
|
||||||
|
.thenReturn(Mono.just(fakeTheme));
|
||||||
|
when(reactiveUrlDataBufferFetcher.fetch(eq(uri)))
|
||||||
|
.thenReturn(Flux.just(mock(DataBuffer.class)));
|
||||||
|
var body = new ThemeEndpoint.UpgradeFromUriRequest(uri);
|
||||||
|
webTestClient.post()
|
||||||
|
.uri("/themes/-/install-from-uri")
|
||||||
|
.bodyValue(body)
|
||||||
|
.exchange()
|
||||||
|
.expectStatus().isOk();
|
||||||
|
|
||||||
|
verify(themeService).install(isA(InputStream.class));
|
||||||
|
verify(reactiveUrlDataBufferFetcher).fetch(eq(uri));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void reloadTheme() {
|
void reloadTheme() {
|
||||||
when(themeService.reloadTheme(any())).thenReturn(Mono.empty());
|
when(themeService.reloadTheme(any())).thenReturn(Mono.empty());
|
||||||
|
|
|
@ -120,6 +120,7 @@ models/group-spec.ts
|
||||||
models/group-status.ts
|
models/group-status.ts
|
||||||
models/group.ts
|
models/group.ts
|
||||||
models/index.ts
|
models/index.ts
|
||||||
|
models/install-from-uri-request.ts
|
||||||
models/license.ts
|
models/license.ts
|
||||||
models/listed-auth-provider.ts
|
models/listed-auth-provider.ts
|
||||||
models/listed-comment-list.ts
|
models/listed-comment-list.ts
|
||||||
|
@ -222,6 +223,7 @@ models/theme-list.ts
|
||||||
models/theme-spec.ts
|
models/theme-spec.ts
|
||||||
models/theme-status.ts
|
models/theme-status.ts
|
||||||
models/theme.ts
|
models/theme.ts
|
||||||
|
models/upgrade-from-uri-request.ts
|
||||||
models/user-connection-list.ts
|
models/user-connection-list.ts
|
||||||
models/user-connection-spec.ts
|
models/user-connection-spec.ts
|
||||||
models/user-connection.ts
|
models/user-connection.ts
|
||||||
|
|
|
@ -40,11 +40,15 @@ import {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { ConfigMap } from "../models";
|
import { ConfigMap } from "../models";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
import { InstallFromUriRequest } from "../models";
|
||||||
|
// @ts-ignore
|
||||||
import { Setting } from "../models";
|
import { Setting } from "../models";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { Theme } from "../models";
|
import { Theme } from "../models";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { ThemeList } from "../models";
|
import { ThemeList } from "../models";
|
||||||
|
// @ts-ignore
|
||||||
|
import { UpgradeFromUriRequest } from "../models";
|
||||||
/**
|
/**
|
||||||
* ApiConsoleHaloRunV1alpha1ThemeApi - axios parameter creator
|
* ApiConsoleHaloRunV1alpha1ThemeApi - axios parameter creator
|
||||||
* @export
|
* @export
|
||||||
|
@ -321,6 +325,67 @@ export const ApiConsoleHaloRunV1alpha1ThemeApiAxiosParamCreator = function (
|
||||||
options: localVarRequestOptions,
|
options: localVarRequestOptions,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Install a theme from uri.
|
||||||
|
* @param {InstallFromUriRequest} installFromUriRequest
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
installThemeFromUri: async (
|
||||||
|
installFromUriRequest: InstallFromUriRequest,
|
||||||
|
options: AxiosRequestConfig = {}
|
||||||
|
): Promise<RequestArgs> => {
|
||||||
|
// verify required parameter 'installFromUriRequest' is not null or undefined
|
||||||
|
assertParamExists(
|
||||||
|
"installThemeFromUri",
|
||||||
|
"installFromUriRequest",
|
||||||
|
installFromUriRequest
|
||||||
|
);
|
||||||
|
const localVarPath = `/apis/api.console.halo.run/v1alpha1/themes/-/install-from-uri`;
|
||||||
|
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||||
|
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||||
|
let baseOptions;
|
||||||
|
if (configuration) {
|
||||||
|
baseOptions = configuration.baseOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
const localVarRequestOptions = {
|
||||||
|
method: "POST",
|
||||||
|
...baseOptions,
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
const localVarHeaderParameter = {} as any;
|
||||||
|
const localVarQueryParameter = {} as any;
|
||||||
|
|
||||||
|
// authentication BasicAuth required
|
||||||
|
// http basic authentication required
|
||||||
|
setBasicAuthToObject(localVarRequestOptions, configuration);
|
||||||
|
|
||||||
|
// authentication BearerAuth required
|
||||||
|
// http bearer authentication required
|
||||||
|
await setBearerAuthToObject(localVarHeaderParameter, configuration);
|
||||||
|
|
||||||
|
localVarHeaderParameter["Content-Type"] = "application/json";
|
||||||
|
|
||||||
|
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||||
|
let headersFromBaseOptions =
|
||||||
|
baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||||
|
localVarRequestOptions.headers = {
|
||||||
|
...localVarHeaderParameter,
|
||||||
|
...headersFromBaseOptions,
|
||||||
|
...options.headers,
|
||||||
|
};
|
||||||
|
localVarRequestOptions.data = serializeDataIfNeeded(
|
||||||
|
installFromUriRequest,
|
||||||
|
localVarRequestOptions,
|
||||||
|
configuration
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: toPathString(localVarUrlObj),
|
||||||
|
options: localVarRequestOptions,
|
||||||
|
};
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* List themes.
|
* List themes.
|
||||||
* @param {boolean} uninstalled
|
* @param {boolean} uninstalled
|
||||||
|
@ -635,6 +700,75 @@ export const ApiConsoleHaloRunV1alpha1ThemeApiAxiosParamCreator = function (
|
||||||
};
|
};
|
||||||
localVarRequestOptions.data = localVarFormParams;
|
localVarRequestOptions.data = localVarFormParams;
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: toPathString(localVarUrlObj),
|
||||||
|
options: localVarRequestOptions,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Upgrade a theme from uri.
|
||||||
|
* @param {string} name
|
||||||
|
* @param {UpgradeFromUriRequest} upgradeFromUriRequest
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
upgradeThemeFromUri: async (
|
||||||
|
name: string,
|
||||||
|
upgradeFromUriRequest: UpgradeFromUriRequest,
|
||||||
|
options: AxiosRequestConfig = {}
|
||||||
|
): Promise<RequestArgs> => {
|
||||||
|
// verify required parameter 'name' is not null or undefined
|
||||||
|
assertParamExists("upgradeThemeFromUri", "name", name);
|
||||||
|
// verify required parameter 'upgradeFromUriRequest' is not null or undefined
|
||||||
|
assertParamExists(
|
||||||
|
"upgradeThemeFromUri",
|
||||||
|
"upgradeFromUriRequest",
|
||||||
|
upgradeFromUriRequest
|
||||||
|
);
|
||||||
|
const localVarPath =
|
||||||
|
`/apis/api.console.halo.run/v1alpha1/themes/{name}/upgrade-from-uri`.replace(
|
||||||
|
`{${"name"}}`,
|
||||||
|
encodeURIComponent(String(name))
|
||||||
|
);
|
||||||
|
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||||
|
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||||
|
let baseOptions;
|
||||||
|
if (configuration) {
|
||||||
|
baseOptions = configuration.baseOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
const localVarRequestOptions = {
|
||||||
|
method: "POST",
|
||||||
|
...baseOptions,
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
const localVarHeaderParameter = {} as any;
|
||||||
|
const localVarQueryParameter = {} as any;
|
||||||
|
|
||||||
|
// authentication BasicAuth required
|
||||||
|
// http basic authentication required
|
||||||
|
setBasicAuthToObject(localVarRequestOptions, configuration);
|
||||||
|
|
||||||
|
// authentication BearerAuth required
|
||||||
|
// http bearer authentication required
|
||||||
|
await setBearerAuthToObject(localVarHeaderParameter, configuration);
|
||||||
|
|
||||||
|
localVarHeaderParameter["Content-Type"] = "application/json";
|
||||||
|
|
||||||
|
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||||
|
let headersFromBaseOptions =
|
||||||
|
baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||||
|
localVarRequestOptions.headers = {
|
||||||
|
...localVarHeaderParameter,
|
||||||
|
...headersFromBaseOptions,
|
||||||
|
...options.headers,
|
||||||
|
};
|
||||||
|
localVarRequestOptions.data = serializeDataIfNeeded(
|
||||||
|
upgradeFromUriRequest,
|
||||||
|
localVarRequestOptions,
|
||||||
|
configuration
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
url: toPathString(localVarUrlObj),
|
url: toPathString(localVarUrlObj),
|
||||||
options: localVarRequestOptions,
|
options: localVarRequestOptions,
|
||||||
|
@ -760,6 +894,30 @@ export const ApiConsoleHaloRunV1alpha1ThemeApiFp = function (
|
||||||
configuration
|
configuration
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Install a theme from uri.
|
||||||
|
* @param {InstallFromUriRequest} installFromUriRequest
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
async installThemeFromUri(
|
||||||
|
installFromUriRequest: InstallFromUriRequest,
|
||||||
|
options?: AxiosRequestConfig
|
||||||
|
): Promise<
|
||||||
|
(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Theme>
|
||||||
|
> {
|
||||||
|
const localVarAxiosArgs =
|
||||||
|
await localVarAxiosParamCreator.installThemeFromUri(
|
||||||
|
installFromUriRequest,
|
||||||
|
options
|
||||||
|
);
|
||||||
|
return createRequestFunction(
|
||||||
|
localVarAxiosArgs,
|
||||||
|
globalAxios,
|
||||||
|
BASE_PATH,
|
||||||
|
configuration
|
||||||
|
);
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* List themes.
|
* List themes.
|
||||||
* @param {boolean} uninstalled
|
* @param {boolean} uninstalled
|
||||||
|
@ -892,6 +1050,33 @@ export const ApiConsoleHaloRunV1alpha1ThemeApiFp = function (
|
||||||
configuration
|
configuration
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Upgrade a theme from uri.
|
||||||
|
* @param {string} name
|
||||||
|
* @param {UpgradeFromUriRequest} upgradeFromUriRequest
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
async upgradeThemeFromUri(
|
||||||
|
name: string,
|
||||||
|
upgradeFromUriRequest: UpgradeFromUriRequest,
|
||||||
|
options?: AxiosRequestConfig
|
||||||
|
): Promise<
|
||||||
|
(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Theme>
|
||||||
|
> {
|
||||||
|
const localVarAxiosArgs =
|
||||||
|
await localVarAxiosParamCreator.upgradeThemeFromUri(
|
||||||
|
name,
|
||||||
|
upgradeFromUriRequest,
|
||||||
|
options
|
||||||
|
);
|
||||||
|
return createRequestFunction(
|
||||||
|
localVarAxiosArgs,
|
||||||
|
globalAxios,
|
||||||
|
BASE_PATH,
|
||||||
|
configuration
|
||||||
|
);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -972,6 +1157,20 @@ export const ApiConsoleHaloRunV1alpha1ThemeApiFactory = function (
|
||||||
.installTheme(requestParameters.file, options)
|
.installTheme(requestParameters.file, options)
|
||||||
.then((request) => request(axios, basePath));
|
.then((request) => request(axios, basePath));
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Install a theme from uri.
|
||||||
|
* @param {ApiConsoleHaloRunV1alpha1ThemeApiInstallThemeFromUriRequest} requestParameters Request parameters.
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
installThemeFromUri(
|
||||||
|
requestParameters: ApiConsoleHaloRunV1alpha1ThemeApiInstallThemeFromUriRequest,
|
||||||
|
options?: AxiosRequestConfig
|
||||||
|
): AxiosPromise<Theme> {
|
||||||
|
return localVarFp
|
||||||
|
.installThemeFromUri(requestParameters.installFromUriRequest, options)
|
||||||
|
.then((request) => request(axios, basePath));
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* List themes.
|
* List themes.
|
||||||
* @param {ApiConsoleHaloRunV1alpha1ThemeApiListThemesRequest} requestParameters Request parameters.
|
* @param {ApiConsoleHaloRunV1alpha1ThemeApiListThemesRequest} requestParameters Request parameters.
|
||||||
|
@ -1053,6 +1252,24 @@ export const ApiConsoleHaloRunV1alpha1ThemeApiFactory = function (
|
||||||
.upgradeTheme(requestParameters.name, requestParameters.file, options)
|
.upgradeTheme(requestParameters.name, requestParameters.file, options)
|
||||||
.then((request) => request(axios, basePath));
|
.then((request) => request(axios, basePath));
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Upgrade a theme from uri.
|
||||||
|
* @param {ApiConsoleHaloRunV1alpha1ThemeApiUpgradeThemeFromUriRequest} requestParameters Request parameters.
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
upgradeThemeFromUri(
|
||||||
|
requestParameters: ApiConsoleHaloRunV1alpha1ThemeApiUpgradeThemeFromUriRequest,
|
||||||
|
options?: AxiosRequestConfig
|
||||||
|
): AxiosPromise<Theme> {
|
||||||
|
return localVarFp
|
||||||
|
.upgradeThemeFromUri(
|
||||||
|
requestParameters.name,
|
||||||
|
requestParameters.upgradeFromUriRequest,
|
||||||
|
options
|
||||||
|
)
|
||||||
|
.then((request) => request(axios, basePath));
|
||||||
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1112,6 +1329,20 @@ export interface ApiConsoleHaloRunV1alpha1ThemeApiInstallThemeRequest {
|
||||||
readonly file: File;
|
readonly file: File;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request parameters for installThemeFromUri operation in ApiConsoleHaloRunV1alpha1ThemeApi.
|
||||||
|
* @export
|
||||||
|
* @interface ApiConsoleHaloRunV1alpha1ThemeApiInstallThemeFromUriRequest
|
||||||
|
*/
|
||||||
|
export interface ApiConsoleHaloRunV1alpha1ThemeApiInstallThemeFromUriRequest {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {InstallFromUriRequest}
|
||||||
|
* @memberof ApiConsoleHaloRunV1alpha1ThemeApiInstallThemeFromUri
|
||||||
|
*/
|
||||||
|
readonly installFromUriRequest: InstallFromUriRequest;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request parameters for listThemes operation in ApiConsoleHaloRunV1alpha1ThemeApi.
|
* Request parameters for listThemes operation in ApiConsoleHaloRunV1alpha1ThemeApi.
|
||||||
* @export
|
* @export
|
||||||
|
@ -1224,6 +1455,27 @@ export interface ApiConsoleHaloRunV1alpha1ThemeApiUpgradeThemeRequest {
|
||||||
readonly file: File;
|
readonly file: File;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request parameters for upgradeThemeFromUri operation in ApiConsoleHaloRunV1alpha1ThemeApi.
|
||||||
|
* @export
|
||||||
|
* @interface ApiConsoleHaloRunV1alpha1ThemeApiUpgradeThemeFromUriRequest
|
||||||
|
*/
|
||||||
|
export interface ApiConsoleHaloRunV1alpha1ThemeApiUpgradeThemeFromUriRequest {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof ApiConsoleHaloRunV1alpha1ThemeApiUpgradeThemeFromUri
|
||||||
|
*/
|
||||||
|
readonly name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {UpgradeFromUriRequest}
|
||||||
|
* @memberof ApiConsoleHaloRunV1alpha1ThemeApiUpgradeThemeFromUri
|
||||||
|
*/
|
||||||
|
readonly upgradeFromUriRequest: UpgradeFromUriRequest;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ApiConsoleHaloRunV1alpha1ThemeApi - object-oriented interface
|
* ApiConsoleHaloRunV1alpha1ThemeApi - object-oriented interface
|
||||||
* @export
|
* @export
|
||||||
|
@ -1307,6 +1559,22 @@ export class ApiConsoleHaloRunV1alpha1ThemeApi extends BaseAPI {
|
||||||
.then((request) => request(this.axios, this.basePath));
|
.then((request) => request(this.axios, this.basePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Install a theme from uri.
|
||||||
|
* @param {ApiConsoleHaloRunV1alpha1ThemeApiInstallThemeFromUriRequest} requestParameters Request parameters.
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
* @memberof ApiConsoleHaloRunV1alpha1ThemeApi
|
||||||
|
*/
|
||||||
|
public installThemeFromUri(
|
||||||
|
requestParameters: ApiConsoleHaloRunV1alpha1ThemeApiInstallThemeFromUriRequest,
|
||||||
|
options?: AxiosRequestConfig
|
||||||
|
) {
|
||||||
|
return ApiConsoleHaloRunV1alpha1ThemeApiFp(this.configuration)
|
||||||
|
.installThemeFromUri(requestParameters.installFromUriRequest, options)
|
||||||
|
.then((request) => request(this.axios, this.basePath));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List themes.
|
* List themes.
|
||||||
* @param {ApiConsoleHaloRunV1alpha1ThemeApiListThemesRequest} requestParameters Request parameters.
|
* @param {ApiConsoleHaloRunV1alpha1ThemeApiListThemesRequest} requestParameters Request parameters.
|
||||||
|
@ -1397,4 +1665,24 @@ export class ApiConsoleHaloRunV1alpha1ThemeApi extends BaseAPI {
|
||||||
.upgradeTheme(requestParameters.name, requestParameters.file, options)
|
.upgradeTheme(requestParameters.name, requestParameters.file, options)
|
||||||
.then((request) => request(this.axios, this.basePath));
|
.then((request) => request(this.axios, this.basePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upgrade a theme from uri.
|
||||||
|
* @param {ApiConsoleHaloRunV1alpha1ThemeApiUpgradeThemeFromUriRequest} requestParameters Request parameters.
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
* @memberof ApiConsoleHaloRunV1alpha1ThemeApi
|
||||||
|
*/
|
||||||
|
public upgradeThemeFromUri(
|
||||||
|
requestParameters: ApiConsoleHaloRunV1alpha1ThemeApiUpgradeThemeFromUriRequest,
|
||||||
|
options?: AxiosRequestConfig
|
||||||
|
) {
|
||||||
|
return ApiConsoleHaloRunV1alpha1ThemeApiFp(this.configuration)
|
||||||
|
.upgradeThemeFromUri(
|
||||||
|
requestParameters.name,
|
||||||
|
requestParameters.upgradeFromUriRequest,
|
||||||
|
options
|
||||||
|
)
|
||||||
|
.then((request) => request(this.axios, this.basePath));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,6 +58,7 @@ export * from "./group-kind";
|
||||||
export * from "./group-list";
|
export * from "./group-list";
|
||||||
export * from "./group-spec";
|
export * from "./group-spec";
|
||||||
export * from "./group-status";
|
export * from "./group-status";
|
||||||
|
export * from "./install-from-uri-request";
|
||||||
export * from "./license";
|
export * from "./license";
|
||||||
export * from "./listed-auth-provider";
|
export * from "./listed-auth-provider";
|
||||||
export * from "./listed-comment";
|
export * from "./listed-comment";
|
||||||
|
@ -160,6 +161,7 @@ export * from "./theme";
|
||||||
export * from "./theme-list";
|
export * from "./theme-list";
|
||||||
export * from "./theme-spec";
|
export * from "./theme-spec";
|
||||||
export * from "./theme-status";
|
export * from "./theme-status";
|
||||||
|
export * from "./upgrade-from-uri-request";
|
||||||
export * from "./user";
|
export * from "./user";
|
||||||
export * from "./user-connection";
|
export * from "./user-connection";
|
||||||
export * from "./user-connection-list";
|
export * from "./user-connection-list";
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* Halo Next API
|
||||||
|
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
|
||||||
|
*
|
||||||
|
* The version of the OpenAPI document: 2.0.0
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||||
|
* https://openapi-generator.tech
|
||||||
|
* Do not edit the class manually.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @interface InstallFromUriRequest
|
||||||
|
*/
|
||||||
|
export interface InstallFromUriRequest {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof InstallFromUriRequest
|
||||||
|
*/
|
||||||
|
uri: string;
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* Halo Next API
|
||||||
|
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
|
||||||
|
*
|
||||||
|
* The version of the OpenAPI document: 2.0.0
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||||
|
* https://openapi-generator.tech
|
||||||
|
* Do not edit the class manually.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @interface UpgradeFromUriRequest
|
||||||
|
*/
|
||||||
|
export interface UpgradeFromUriRequest {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof UpgradeFromUriRequest
|
||||||
|
*/
|
||||||
|
uri: string;
|
||||||
|
}
|
|
@ -597,10 +597,19 @@ core:
|
||||||
uninstall_and_delete_config:
|
uninstall_and_delete_config:
|
||||||
button: Uninstall and delete config
|
button: Uninstall and delete config
|
||||||
title: Are you sure you want to uninstall this theme and its corresponding settings?
|
title: Are you sure you want to uninstall this theme and its corresponding settings?
|
||||||
|
remote_download:
|
||||||
|
title: Remote download address detected, do you want to download?
|
||||||
|
description: "Please carefully verify whether this address can be trusted: {url}"
|
||||||
upload_modal:
|
upload_modal:
|
||||||
titles:
|
titles:
|
||||||
install: Install theme
|
install: Install theme
|
||||||
upgrade: Upgrade theme ({display_name})
|
upgrade: Upgrade theme ({display_name})
|
||||||
|
tabs:
|
||||||
|
local: Local
|
||||||
|
remote:
|
||||||
|
title: Remote
|
||||||
|
fields:
|
||||||
|
url: Remote URL
|
||||||
list_modal:
|
list_modal:
|
||||||
titles:
|
titles:
|
||||||
installed_themes: Installed Themes
|
installed_themes: Installed Themes
|
||||||
|
|
|
@ -597,10 +597,19 @@ core:
|
||||||
uninstall_and_delete_config:
|
uninstall_and_delete_config:
|
||||||
button: 卸载并删除配置
|
button: 卸载并删除配置
|
||||||
title: 确定要卸载该主题以及对应的配置吗?
|
title: 确定要卸载该主题以及对应的配置吗?
|
||||||
|
remote_download:
|
||||||
|
title: 检测到了远程下载地址,是否需要下载?
|
||||||
|
description: 请仔细鉴别此地址是否可信:{url}
|
||||||
upload_modal:
|
upload_modal:
|
||||||
titles:
|
titles:
|
||||||
install: 安装主题
|
install: 安装主题
|
||||||
upgrade: 升级主题({display_name})
|
upgrade: 升级主题({display_name})
|
||||||
|
tabs:
|
||||||
|
local: 本地上传
|
||||||
|
remote:
|
||||||
|
title: 远程下载
|
||||||
|
fields:
|
||||||
|
url: 下载地址
|
||||||
list_modal:
|
list_modal:
|
||||||
titles:
|
titles:
|
||||||
installed_themes: 已安装的主题
|
installed_themes: 已安装的主题
|
||||||
|
|
|
@ -597,10 +597,19 @@ core:
|
||||||
uninstall_and_delete_config:
|
uninstall_and_delete_config:
|
||||||
button: 卸載並刪除配置
|
button: 卸載並刪除配置
|
||||||
title: 確定要卸載該主題以及對應的配置嗎?
|
title: 確定要卸載該主題以及對應的配置嗎?
|
||||||
|
remote_download:
|
||||||
|
title: 偵測到遠端下載地址,是否需要下載?
|
||||||
|
description: 請仔細鑑別此地址是否可信:{url}
|
||||||
upload_modal:
|
upload_modal:
|
||||||
titles:
|
titles:
|
||||||
install: 安裝主題
|
install: 安裝主題
|
||||||
upgrade: 升級主題({display_name})
|
upgrade: 升級主題({display_name})
|
||||||
|
tabs:
|
||||||
|
local: 本地上傳
|
||||||
|
remote:
|
||||||
|
title: 遠端下載
|
||||||
|
fields:
|
||||||
|
url: 下載地址
|
||||||
list_modal:
|
list_modal:
|
||||||
titles:
|
titles:
|
||||||
installed_themes: 已安裝的主題
|
installed_themes: 已安裝的主題
|
||||||
|
|
|
@ -17,11 +17,12 @@ import LazyImage from "@/components/image/LazyImage.vue";
|
||||||
import ThemePreviewModal from "./preview/ThemePreviewModal.vue";
|
import ThemePreviewModal from "./preview/ThemePreviewModal.vue";
|
||||||
import ThemeUploadModal from "./ThemeUploadModal.vue";
|
import ThemeUploadModal from "./ThemeUploadModal.vue";
|
||||||
import ThemeListItem from "./components/ThemeListItem.vue";
|
import ThemeListItem from "./components/ThemeListItem.vue";
|
||||||
import { computed, ref } from "vue";
|
import { computed, ref, nextTick, watch } from "vue";
|
||||||
import type { Theme } from "@halo-dev/api-client";
|
import type { Theme } from "@halo-dev/api-client";
|
||||||
import { apiClient } from "@/utils/api-client";
|
import { apiClient } from "@/utils/api-client";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { useQuery } from "@tanstack/vue-query";
|
import { useQuery } from "@tanstack/vue-query";
|
||||||
|
import { useRouteQuery } from "@vueuse/router";
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
@ -143,6 +144,19 @@ const handleOpenInstallModal = () => {
|
||||||
themeToUpgrade.value = undefined;
|
themeToUpgrade.value = undefined;
|
||||||
themeUploadVisible.value = true;
|
themeUploadVisible.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// handle remote wordpress url from route
|
||||||
|
const remoteDownloadUrl = useRouteQuery<string>("remote-download-url");
|
||||||
|
watch(
|
||||||
|
() => props.visible,
|
||||||
|
(visible) => {
|
||||||
|
if (visible && remoteDownloadUrl.value) {
|
||||||
|
nextTick(() => {
|
||||||
|
handleOpenInstallModal();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<VModal
|
<VModal
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { Toast, VModal } from "@halo-dev/components";
|
import { Toast, VButton, VModal, VTabItem, VTabs } from "@halo-dev/components";
|
||||||
import UppyUpload from "@/components/upload/UppyUpload.vue";
|
import UppyUpload from "@/components/upload/UppyUpload.vue";
|
||||||
import { computed, ref, watch } from "vue";
|
import { computed, ref, watch, nextTick } from "vue";
|
||||||
import type { Theme } from "@halo-dev/api-client";
|
import type { Theme } from "@halo-dev/api-client";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { useQueryClient } from "@tanstack/vue-query";
|
import { useQueryClient } from "@tanstack/vue-query";
|
||||||
import { useThemeStore } from "@/stores/theme";
|
import { useThemeStore } from "@/stores/theme";
|
||||||
|
import { apiClient } from "@/utils/api-client";
|
||||||
|
import { useRouteQuery } from "@vueuse/router";
|
||||||
|
import { submitForm } from "@formkit/core";
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
@ -79,14 +82,79 @@ const onUploaded = () => {
|
||||||
|
|
||||||
handleVisibleChange(false);
|
handleVisibleChange(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// remote download
|
||||||
|
const activeTabId = ref("local");
|
||||||
|
const remoteDownloadUrl = ref("");
|
||||||
|
const downloading = ref(false);
|
||||||
|
|
||||||
|
const handleDownloadTheme = async () => {
|
||||||
|
try {
|
||||||
|
downloading.value = true;
|
||||||
|
|
||||||
|
if (props.upgradeTheme) {
|
||||||
|
await apiClient.theme.upgradeThemeFromUri({
|
||||||
|
name: props.upgradeTheme.metadata.name,
|
||||||
|
upgradeFromUriRequest: {
|
||||||
|
uri: remoteDownloadUrl.value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await apiClient.theme.installThemeFromUri({
|
||||||
|
installFromUriRequest: {
|
||||||
|
uri: remoteDownloadUrl.value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Toast.success(
|
||||||
|
t(
|
||||||
|
props.upgradeTheme
|
||||||
|
? "core.common.toast.upgrade_success"
|
||||||
|
: "core.common.toast.install_success"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["themes"] });
|
||||||
|
themeStore.fetchActivatedTheme();
|
||||||
|
|
||||||
|
handleVisibleChange(false);
|
||||||
|
|
||||||
|
routeRemoteDownloadUrl.value = null;
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Failed to download theme", error);
|
||||||
|
} finally {
|
||||||
|
downloading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// handle remote download url from route
|
||||||
|
const routeRemoteDownloadUrl = useRouteQuery<string | null>(
|
||||||
|
"remote-download-url"
|
||||||
|
);
|
||||||
|
watch(
|
||||||
|
() => props.visible,
|
||||||
|
(visible) => {
|
||||||
|
if (routeRemoteDownloadUrl.value && visible) {
|
||||||
|
activeTabId.value = "remote";
|
||||||
|
remoteDownloadUrl.value = routeRemoteDownloadUrl.value as string;
|
||||||
|
nextTick(() => {
|
||||||
|
submitForm("theme-remote-download-form");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<VModal
|
<VModal
|
||||||
:visible="visible"
|
:visible="visible"
|
||||||
:width="600"
|
:width="600"
|
||||||
:title="modalTitle"
|
:title="modalTitle"
|
||||||
|
:centered="false"
|
||||||
@update:visible="handleVisibleChange"
|
@update:visible="handleVisibleChange"
|
||||||
>
|
>
|
||||||
|
<VTabs v-model:active-id="activeTabId" type="outline" class="!rounded-none">
|
||||||
|
<VTabItem id="local" :label="$t('core.theme.upload_modal.tabs.local')">
|
||||||
<UppyUpload
|
<UppyUpload
|
||||||
v-if="uploadVisible"
|
v-if="uploadVisible"
|
||||||
:restrictions="{
|
:restrictions="{
|
||||||
|
@ -97,5 +165,35 @@ const onUploaded = () => {
|
||||||
auto-proceed
|
auto-proceed
|
||||||
@uploaded="onUploaded"
|
@uploaded="onUploaded"
|
||||||
/>
|
/>
|
||||||
|
</VTabItem>
|
||||||
|
<VTabItem
|
||||||
|
id="remote"
|
||||||
|
:label="$t('core.theme.upload_modal.tabs.remote.title')"
|
||||||
|
>
|
||||||
|
<FormKit
|
||||||
|
id="theme-remote-download-form"
|
||||||
|
name="theme-remote-download-form"
|
||||||
|
type="form"
|
||||||
|
:preserve="true"
|
||||||
|
@submit="handleDownloadTheme"
|
||||||
|
>
|
||||||
|
<FormKit
|
||||||
|
v-model="remoteDownloadUrl"
|
||||||
|
:label="$t('core.theme.upload_modal.tabs.remote.fields.url')"
|
||||||
|
type="text"
|
||||||
|
></FormKit>
|
||||||
|
</FormKit>
|
||||||
|
|
||||||
|
<div class="pt-5">
|
||||||
|
<VButton
|
||||||
|
:loading="downloading"
|
||||||
|
type="secondary"
|
||||||
|
@click="$formkit.submit('theme-remote-download-form')"
|
||||||
|
>
|
||||||
|
{{ $t("core.common.buttons.download") }}
|
||||||
|
</VButton>
|
||||||
|
</div>
|
||||||
|
</VTabItem>
|
||||||
|
</VTabs>
|
||||||
</VModal>
|
</VModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -24,6 +24,7 @@ import {
|
||||||
VSpace,
|
VSpace,
|
||||||
VTabbar,
|
VTabbar,
|
||||||
VLoading,
|
VLoading,
|
||||||
|
Dialog,
|
||||||
} from "@halo-dev/components";
|
} from "@halo-dev/components";
|
||||||
import ThemeListModal from "../components/ThemeListModal.vue";
|
import ThemeListModal from "../components/ThemeListModal.vue";
|
||||||
import ThemePreviewModal from "../components/preview/ThemePreviewModal.vue";
|
import ThemePreviewModal from "../components/preview/ThemePreviewModal.vue";
|
||||||
|
@ -34,6 +35,7 @@ import { storeToRefs } from "pinia";
|
||||||
import { apiClient } from "@/utils/api-client";
|
import { apiClient } from "@/utils/api-client";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { useQuery } from "@tanstack/vue-query";
|
import { useQuery } from "@tanstack/vue-query";
|
||||||
|
import { useRouteQuery } from "@vueuse/router";
|
||||||
|
|
||||||
const { currentUserHasPermission } = usePermission();
|
const { currentUserHasPermission } = usePermission();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
@ -157,6 +159,27 @@ onMounted(() => {
|
||||||
watch([() => route.name, () => route.params], async () => {
|
watch([() => route.name, () => route.params], async () => {
|
||||||
handleTriggerTabChange();
|
handleTriggerTabChange();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// handle remote download url from route
|
||||||
|
const remoteDownloadUrl = useRouteQuery<string | null>("remote-download-url");
|
||||||
|
onMounted(() => {
|
||||||
|
if (remoteDownloadUrl.value) {
|
||||||
|
Dialog.warning({
|
||||||
|
title: t("core.theme.operations.remote_download.title"),
|
||||||
|
description: t("core.theme.operations.remote_download.description", {
|
||||||
|
url: remoteDownloadUrl.value,
|
||||||
|
}),
|
||||||
|
confirmText: t("core.common.buttons.download"),
|
||||||
|
cancelText: t("core.common.buttons.cancel"),
|
||||||
|
onConfirm() {
|
||||||
|
themesModal.value = true;
|
||||||
|
},
|
||||||
|
onCancel() {
|
||||||
|
remoteDownloadUrl.value = null;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<BasicLayout>
|
<BasicLayout>
|
||||||
|
|
Loading…
Reference in New Issue