From 170cf4e4121178a940a22a55683153c8559d8435 Mon Sep 17 00:00:00 2001 From: guqing <38999863+guqing@users.noreply.github.com> Date: Fri, 19 May 2023 10:10:24 +0800 Subject: [PATCH] feat: add the ability to install themes remotely via URI (#3939) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### 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 --- .../core/extension/theme/ThemeEndpoint.java | 86 ++++++ .../app/core/extension/theme/ThemeUtils.java | 19 +- .../DefaultReactiveUrlDataBufferFetcher.java | 34 +++ .../infra/ReactiveUrlDataBufferFetcher.java | 23 ++ .../ThemeAlreadyExistsException.java | 29 ++ .../extensions/role-template-theme.yaml | 3 +- .../extension/theme/ThemeEndpointTest.java | 53 ++++ .../api-client/src/.openapi-generator/FILES | 2 + ...api-console-halo-run-v1alpha1-theme-api.ts | 288 ++++++++++++++++++ .../packages/api-client/src/models/index.ts | 2 + .../src/models/install-from-uri-request.ts | 27 ++ .../src/models/upgrade-from-uri-request.ts | 27 ++ console/src/locales/en.yaml | 9 + console/src/locales/zh-CN.yaml | 9 + console/src/locales/zh-TW.yaml | 9 + .../themes/components/ThemeListModal.vue | 16 +- .../themes/components/ThemeUploadModal.vue | 122 +++++++- .../interface/themes/layouts/ThemeLayout.vue | 23 ++ 18 files changed, 763 insertions(+), 18 deletions(-) create mode 100644 application/src/main/java/run/halo/app/infra/DefaultReactiveUrlDataBufferFetcher.java create mode 100644 application/src/main/java/run/halo/app/infra/ReactiveUrlDataBufferFetcher.java create mode 100644 application/src/main/java/run/halo/app/infra/exception/ThemeAlreadyExistsException.java create mode 100644 console/packages/api-client/src/models/install-from-uri-request.ts create mode 100644 console/packages/api-client/src/models/upgrade-from-uri-request.ts diff --git a/application/src/main/java/run/halo/app/core/extension/theme/ThemeEndpoint.java b/application/src/main/java/run/halo/app/core/extension/theme/ThemeEndpoint.java index 223d66375..e6f215439 100644 --- a/application/src/main/java/run/halo/app/core/extension/theme/ThemeEndpoint.java +++ b/application/src/main/java/run/halo/app/core/extension/theme/ThemeEndpoint.java @@ -1,5 +1,6 @@ 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.content.Builder.contentBuilder; 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.media.Schema; import java.io.IOException; +import java.net.URI; import java.nio.file.Path; import java.nio.file.Paths; 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.router.IListRequest; import run.halo.app.extension.router.QueryParamBuildUtil; +import run.halo.app.infra.ReactiveUrlDataBufferFetcher; import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; import run.halo.app.infra.SystemSetting; import run.halo.app.infra.ThemeRootGetter; 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.theme.TemplateEngineManager; @@ -71,6 +77,8 @@ public class ThemeEndpoint implements CustomEndpoint { private final SystemConfigurableEnvironmentFetcher systemEnvironmentFetcher; + private final ReactiveUrlDataBufferFetcher reactiveUrlDataBufferFetcher; + @Override public RouterFunction endpoint() { final var tag = "api.console.halo.run/v1alpha1/Theme"; @@ -89,6 +97,39 @@ public class ThemeEndpoint implements CustomEndpoint { .response(responseBuilder() .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, builder -> builder.operationId("UpgradeTheme") .description("Upgrade theme") @@ -200,6 +241,45 @@ public class ThemeEndpoint implements CustomEndpoint { .build(); } + private Mono 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 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 activateTheme(ServerRequest request) { final var activatedThemeName = request.pathVariable("name"); 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 { private final MultiValueMap multipartData; @@ -418,6 +501,9 @@ public class ThemeEndpoint implements CustomEndpoint { @Schema(required = true, description = "Theme zip file.") FilePart file) { } + public record InstallFromUriRequest(@Schema(requiredMode = REQUIRED) URI uri) { + } + Mono install(ServerRequest request) { return request.body(BodyExtractors.toMultipartData()) .flatMap(this::getZipFilePart) diff --git a/application/src/main/java/run/halo/app/core/extension/theme/ThemeUtils.java b/application/src/main/java/run/halo/app/core/extension/theme/ThemeUtils.java index 987dc6f4d..6de1b3f03 100644 --- a/application/src/main/java/run/halo/app/core/extension/theme/ThemeUtils.java +++ b/application/src/main/java/run/halo/app/core/extension/theme/ThemeUtils.java @@ -19,20 +19,25 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.stream.BaseStream; import java.util.stream.Stream; import java.util.zip.ZipInputStream; +import lombok.extern.slf4j.Slf4j; import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.web.server.ServerWebInputException; import reactor.core.Exceptions; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; import run.halo.app.core.extension.Theme; 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.utils.FileUtils; import run.halo.app.infra.utils.YamlUnstructuredLoader; +@Slf4j class ThemeUtils { private static final String THEME_TMP_PREFIX = "halo-theme-"; private static final String[] THEME_MANIFESTS = {"theme.yaml", "theme.yml"}; @@ -97,7 +102,11 @@ class ThemeUtils { } static Mono 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 unzipThemeTo(InputStream inputStream, Path themeWorkDir, @@ -130,8 +139,7 @@ class ThemeUtils { var themeTargetPath = themeWorkDir.resolve(themeName); try { if (!override && !FileUtils.isEmpty(themeTargetPath)) { - throw new ThemeInstallationException("Theme already exists.", - "problemDetail.theme.install.alreadyExists", new Object[] {themeName}); + throw new ThemeAlreadyExistsException(themeName); } // install theme to theme work dir copyRecursively(themeManifestPath.getParent(), themeTargetPath); @@ -141,7 +149,10 @@ class ThemeUtils { throw Exceptions.propagate(e); } }) - .doFinally(signalType -> deleteRecursivelyAndSilently(tempDir.get())); + .doFinally(signalType -> { + FileUtils.closeQuietly(inputStream); + deleteRecursivelyAndSilently(tempDir.get()); + }); } static Unstructured loadThemeManifest(Path themeManifestPath) { diff --git a/application/src/main/java/run/halo/app/infra/DefaultReactiveUrlDataBufferFetcher.java b/application/src/main/java/run/halo/app/infra/DefaultReactiveUrlDataBufferFetcher.java new file mode 100644 index 000000000..822c3c63b --- /dev/null +++ b/application/src/main/java/run/halo/app/infra/DefaultReactiveUrlDataBufferFetcher.java @@ -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; + +/** + *

A default implementation of {@link ReactiveUrlDataBufferFetcher}.

+ * + * @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 fetch(URI uri) { + return webClient.get() + .uri(uri) + .accept(MediaType.APPLICATION_OCTET_STREAM) + .retrieve() + .bodyToFlux(DataBuffer.class); + } +} diff --git a/application/src/main/java/run/halo/app/infra/ReactiveUrlDataBufferFetcher.java b/application/src/main/java/run/halo/app/infra/ReactiveUrlDataBufferFetcher.java new file mode 100644 index 000000000..df37f04b0 --- /dev/null +++ b/application/src/main/java/run/halo/app/infra/ReactiveUrlDataBufferFetcher.java @@ -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; + +/** + *

{@link DataBuffer} stream fetcher from uri.

+ * + * @author guqing + * @since 2.6.0 + */ +@FunctionalInterface +public interface ReactiveUrlDataBufferFetcher { + + /** + *

Fetch data buffer flux from uri.

+ * + * @param uri uri to fetch + * @return data buffer flux + */ + Flux fetch(URI uri); +} diff --git a/application/src/main/java/run/halo/app/infra/exception/ThemeAlreadyExistsException.java b/application/src/main/java/run/halo/app/infra/exception/ThemeAlreadyExistsException.java new file mode 100644 index 000000000..3cee88f5b --- /dev/null +++ b/application/src/main/java/run/halo/app/infra/exception/ThemeAlreadyExistsException.java @@ -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); + } +} diff --git a/application/src/main/resources/extensions/role-template-theme.yaml b/application/src/main/resources/extensions/role-template-theme.yaml index d2e5c1512..1c10530e6 100644 --- a/application/src/main/resources/extensions/role-template-theme.yaml +++ b/application/src/main/resources/extensions/role-template-theme.yaml @@ -15,7 +15,8 @@ rules: resources: [ "themes" ] verbs: [ "*" ] - 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: [ "*" ] - nonResourceURLs: [ "/apis/api.console.halo.run/themes/install" ] verbs: [ "create" ] diff --git a/application/src/test/java/run/halo/app/core/extension/theme/ThemeEndpointTest.java b/application/src/test/java/run/halo/app/core/extension/theme/ThemeEndpointTest.java index 320d19628..7425fad48 100644 --- a/application/src/test/java/run/halo/app/core/extension/theme/ThemeEndpointTest.java +++ b/application/src/test/java/run/halo/app/core/extension/theme/ThemeEndpointTest.java @@ -4,6 +4,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; 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.IOException; import java.io.InputStream; +import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; import org.junit.jupiter.api.AfterEach; @@ -23,18 +25,21 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.MediaType; import org.springframework.http.client.MultipartBodyBuilder; import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.util.FileSystemUtils; import org.springframework.util.ResourceUtils; import org.springframework.web.server.ServerWebInputException; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; 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.Metadata; import run.halo.app.extension.ReactiveExtensionClient; +import run.halo.app.infra.ReactiveUrlDataBufferFetcher; import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; import run.halo.app.infra.SystemSetting; import run.halo.app.infra.ThemeRootGetter; @@ -64,6 +69,9 @@ class ThemeEndpointTest { @Mock private SystemConfigurableEnvironmentFetcher environmentFetcher; + @Mock + private ReactiveUrlDataBufferFetcher reactiveUrlDataBufferFetcher; + @InjectMocks ThemeEndpoint themeEndpoint; @@ -138,6 +146,32 @@ class ThemeEndpointTest { 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 @@ -173,6 +207,25 @@ class ThemeEndpointTest { .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 void reloadTheme() { when(themeService.reloadTheme(any())).thenReturn(Mono.empty()); diff --git a/console/packages/api-client/src/.openapi-generator/FILES b/console/packages/api-client/src/.openapi-generator/FILES index 43cc546d0..c370375b3 100644 --- a/console/packages/api-client/src/.openapi-generator/FILES +++ b/console/packages/api-client/src/.openapi-generator/FILES @@ -120,6 +120,7 @@ models/group-spec.ts models/group-status.ts models/group.ts models/index.ts +models/install-from-uri-request.ts models/license.ts models/listed-auth-provider.ts models/listed-comment-list.ts @@ -222,6 +223,7 @@ models/theme-list.ts models/theme-spec.ts models/theme-status.ts models/theme.ts +models/upgrade-from-uri-request.ts models/user-connection-list.ts models/user-connection-spec.ts models/user-connection.ts diff --git a/console/packages/api-client/src/api/api-console-halo-run-v1alpha1-theme-api.ts b/console/packages/api-client/src/api/api-console-halo-run-v1alpha1-theme-api.ts index 8e0ada19e..5810c56f4 100644 --- a/console/packages/api-client/src/api/api-console-halo-run-v1alpha1-theme-api.ts +++ b/console/packages/api-client/src/api/api-console-halo-run-v1alpha1-theme-api.ts @@ -40,11 +40,15 @@ import { // @ts-ignore import { ConfigMap } from "../models"; // @ts-ignore +import { InstallFromUriRequest } from "../models"; +// @ts-ignore import { Setting } from "../models"; // @ts-ignore import { Theme } from "../models"; // @ts-ignore import { ThemeList } from "../models"; +// @ts-ignore +import { UpgradeFromUriRequest } from "../models"; /** * ApiConsoleHaloRunV1alpha1ThemeApi - axios parameter creator * @export @@ -321,6 +325,67 @@ export const ApiConsoleHaloRunV1alpha1ThemeApiAxiosParamCreator = function ( 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 => { + // 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. * @param {boolean} uninstalled @@ -635,6 +700,75 @@ export const ApiConsoleHaloRunV1alpha1ThemeApiAxiosParamCreator = function ( }; 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 => { + // 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 { url: toPathString(localVarUrlObj), options: localVarRequestOptions, @@ -760,6 +894,30 @@ export const ApiConsoleHaloRunV1alpha1ThemeApiFp = function ( 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 + > { + const localVarAxiosArgs = + await localVarAxiosParamCreator.installThemeFromUri( + installFromUriRequest, + options + ); + return createRequestFunction( + localVarAxiosArgs, + globalAxios, + BASE_PATH, + configuration + ); + }, /** * List themes. * @param {boolean} uninstalled @@ -892,6 +1050,33 @@ export const ApiConsoleHaloRunV1alpha1ThemeApiFp = function ( 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 + > { + 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) .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 { + return localVarFp + .installThemeFromUri(requestParameters.installFromUriRequest, options) + .then((request) => request(axios, basePath)); + }, /** * List themes. * @param {ApiConsoleHaloRunV1alpha1ThemeApiListThemesRequest} requestParameters Request parameters. @@ -1053,6 +1252,24 @@ export const ApiConsoleHaloRunV1alpha1ThemeApiFactory = function ( .upgradeTheme(requestParameters.name, requestParameters.file, options) .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 { + return localVarFp + .upgradeThemeFromUri( + requestParameters.name, + requestParameters.upgradeFromUriRequest, + options + ) + .then((request) => request(axios, basePath)); + }, }; }; @@ -1112,6 +1329,20 @@ export interface ApiConsoleHaloRunV1alpha1ThemeApiInstallThemeRequest { 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. * @export @@ -1224,6 +1455,27 @@ export interface ApiConsoleHaloRunV1alpha1ThemeApiUpgradeThemeRequest { 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 * @export @@ -1307,6 +1559,22 @@ export class ApiConsoleHaloRunV1alpha1ThemeApi extends BaseAPI { .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. * @param {ApiConsoleHaloRunV1alpha1ThemeApiListThemesRequest} requestParameters Request parameters. @@ -1397,4 +1665,24 @@ export class ApiConsoleHaloRunV1alpha1ThemeApi extends BaseAPI { .upgradeTheme(requestParameters.name, requestParameters.file, options) .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)); + } } diff --git a/console/packages/api-client/src/models/index.ts b/console/packages/api-client/src/models/index.ts index cfe523382..56548cc50 100644 --- a/console/packages/api-client/src/models/index.ts +++ b/console/packages/api-client/src/models/index.ts @@ -58,6 +58,7 @@ export * from "./group-kind"; export * from "./group-list"; export * from "./group-spec"; export * from "./group-status"; +export * from "./install-from-uri-request"; export * from "./license"; export * from "./listed-auth-provider"; export * from "./listed-comment"; @@ -160,6 +161,7 @@ export * from "./theme"; export * from "./theme-list"; export * from "./theme-spec"; export * from "./theme-status"; +export * from "./upgrade-from-uri-request"; export * from "./user"; export * from "./user-connection"; export * from "./user-connection-list"; diff --git a/console/packages/api-client/src/models/install-from-uri-request.ts b/console/packages/api-client/src/models/install-from-uri-request.ts new file mode 100644 index 000000000..b0f584e9a --- /dev/null +++ b/console/packages/api-client/src/models/install-from-uri-request.ts @@ -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; +} diff --git a/console/packages/api-client/src/models/upgrade-from-uri-request.ts b/console/packages/api-client/src/models/upgrade-from-uri-request.ts new file mode 100644 index 000000000..370091d4a --- /dev/null +++ b/console/packages/api-client/src/models/upgrade-from-uri-request.ts @@ -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; +} diff --git a/console/src/locales/en.yaml b/console/src/locales/en.yaml index 4add1c417..c914a7e2a 100644 --- a/console/src/locales/en.yaml +++ b/console/src/locales/en.yaml @@ -597,10 +597,19 @@ core: uninstall_and_delete_config: button: Uninstall and delete config 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: titles: install: Install theme upgrade: Upgrade theme ({display_name}) + tabs: + local: Local + remote: + title: Remote + fields: + url: Remote URL list_modal: titles: installed_themes: Installed Themes diff --git a/console/src/locales/zh-CN.yaml b/console/src/locales/zh-CN.yaml index 0e6cfedd9..70f1659b4 100644 --- a/console/src/locales/zh-CN.yaml +++ b/console/src/locales/zh-CN.yaml @@ -597,10 +597,19 @@ core: uninstall_and_delete_config: button: 卸载并删除配置 title: 确定要卸载该主题以及对应的配置吗? + remote_download: + title: 检测到了远程下载地址,是否需要下载? + description: 请仔细鉴别此地址是否可信:{url} upload_modal: titles: install: 安装主题 upgrade: 升级主题({display_name}) + tabs: + local: 本地上传 + remote: + title: 远程下载 + fields: + url: 下载地址 list_modal: titles: installed_themes: 已安装的主题 diff --git a/console/src/locales/zh-TW.yaml b/console/src/locales/zh-TW.yaml index a8f69e3c0..825b4fdd0 100644 --- a/console/src/locales/zh-TW.yaml +++ b/console/src/locales/zh-TW.yaml @@ -597,10 +597,19 @@ core: uninstall_and_delete_config: button: 卸載並刪除配置 title: 確定要卸載該主題以及對應的配置嗎? + remote_download: + title: 偵測到遠端下載地址,是否需要下載? + description: 請仔細鑑別此地址是否可信:{url} upload_modal: titles: install: 安裝主題 upgrade: 升級主題({display_name}) + tabs: + local: 本地上傳 + remote: + title: 遠端下載 + fields: + url: 下載地址 list_modal: titles: installed_themes: 已安裝的主題 diff --git a/console/src/modules/interface/themes/components/ThemeListModal.vue b/console/src/modules/interface/themes/components/ThemeListModal.vue index a6fe5adee..be1b78510 100644 --- a/console/src/modules/interface/themes/components/ThemeListModal.vue +++ b/console/src/modules/interface/themes/components/ThemeListModal.vue @@ -17,11 +17,12 @@ import LazyImage from "@/components/image/LazyImage.vue"; import ThemePreviewModal from "./preview/ThemePreviewModal.vue"; import ThemeUploadModal from "./ThemeUploadModal.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 { apiClient } from "@/utils/api-client"; import { useI18n } from "vue-i18n"; import { useQuery } from "@tanstack/vue-query"; +import { useRouteQuery } from "@vueuse/router"; const { t } = useI18n(); @@ -143,6 +144,19 @@ const handleOpenInstallModal = () => { themeToUpgrade.value = undefined; themeUploadVisible.value = true; }; + +// handle remote wordpress url from route +const remoteDownloadUrl = useRouteQuery("remote-download-url"); +watch( + () => props.visible, + (visible) => { + if (visible && remoteDownloadUrl.value) { + nextTick(() => { + handleOpenInstallModal(); + }); + } + } +);