mirror of https://github.com/halo-dev/halo
feat: add the ability to install plugins remotely via URI (#3963)
#### What type of PR is this? /kind feature /area core /area console /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/plugins/-/install-from-uri --data '{ "uri": "https://halo.run/apis/api.store.halo.run/v1alpha1/applications/app-KhIVw/releases/app-release-canxF/download/app-release-canxF-znFre" }' ``` 2. 测试插件升级 ```shell curl -u admin:admin -X POST http://localhost:8090/apis/api.console.halo.run/v1alpha1/plugins/PluginFeed/upgrade-from-uri --data '{ "uri": "https://halo.run/apis/api.store.halo.run/v1alpha1/applications/app-KhIVw/releases/app-release-canxF/download/app-release-canxF-znFre" }' ``` #### Which issue(s) this PR fixes: Fixes #2292 #### Does this PR introduce a user-facing change? ```release-note 支持通过 URI 远程安装和升级插件 ```pull/4005/head
parent
f5493a6d86
commit
710261b035
|
@ -1,6 +1,7 @@
|
|||
package run.halo.app.core.extension.endpoint;
|
||||
|
||||
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED;
|
||||
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
|
||||
import static java.util.Comparator.comparing;
|
||||
import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;
|
||||
import static org.springdoc.core.fn.builders.content.Builder.contentBuilder;
|
||||
|
@ -17,6 +18,8 @@ import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
|||
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
@ -38,6 +41,7 @@ import org.springframework.http.codec.multipart.FilePart;
|
|||
import org.springframework.http.codec.multipart.FormFieldPart;
|
||||
import org.springframework.http.codec.multipart.Part;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
|
@ -56,6 +60,10 @@ import run.halo.app.extension.Comparators;
|
|||
import run.halo.app.extension.ConfigMap;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.extension.router.IListRequest.QueryListRequest;
|
||||
import run.halo.app.infra.ReactiveUrlDataBufferFetcher;
|
||||
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.plugin.PluginNotFoundException;
|
||||
|
||||
@Slf4j
|
||||
|
@ -67,6 +75,8 @@ public class PluginEndpoint implements CustomEndpoint {
|
|||
|
||||
private final PluginService pluginService;
|
||||
|
||||
private final ReactiveUrlDataBufferFetcher reactiveUrlDataBufferFetcher;
|
||||
|
||||
@Override
|
||||
public RouterFunction<ServerResponse> endpoint() {
|
||||
final var tag = "api.console.halo.run/v1alpha1/Plugin";
|
||||
|
@ -83,6 +93,39 @@ public class PluginEndpoint implements CustomEndpoint {
|
|||
))
|
||||
.response(responseBuilder().implementation(Plugin.class))
|
||||
)
|
||||
.POST("plugins/-/install-from-uri", this::installFromUri,
|
||||
builder -> builder.operationId("InstallPluginFromUri")
|
||||
.description("Install a plugin from uri.")
|
||||
.tag(tag)
|
||||
.requestBody(requestBodyBuilder()
|
||||
.required(true)
|
||||
.content(contentBuilder()
|
||||
.mediaType(MediaType.APPLICATION_JSON_VALUE)
|
||||
.schema(schemaBuilder()
|
||||
.implementation(InstallFromUriRequest.class))
|
||||
))
|
||||
.response(responseBuilder()
|
||||
.implementation(Plugin.class))
|
||||
)
|
||||
.POST("plugins/{name}/upgrade-from-uri", this::upgradeFromUri,
|
||||
builder -> builder.operationId("UpgradePluginFromUri")
|
||||
.description("Upgrade a plugin 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(Plugin.class))
|
||||
)
|
||||
.POST("plugins/{name}/upgrade", contentType(MediaType.MULTIPART_FORM_DATA),
|
||||
this::upgrade, builder -> builder.operationId("UpgradePlugin")
|
||||
.description("Upgrade a plugin by uploading a Jar file")
|
||||
|
@ -177,6 +220,57 @@ public class PluginEndpoint implements CustomEndpoint {
|
|||
.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())))
|
||||
)
|
||||
.subscribeOn(Schedulers.boundedElastic())
|
||||
.onErrorMap(throwable -> {
|
||||
log.error("Failed to fetch plugin file from uri.", throwable);
|
||||
return new ThemeUpgradeException("Failed to fetch plugin file from uri.", null,
|
||||
null);
|
||||
})
|
||||
.flatMap(inputStream -> Mono.usingWhen(
|
||||
transferToTemp(inputStream),
|
||||
(path) -> pluginService.upgrade(name, path),
|
||||
this::deleteFileIfExists)
|
||||
)
|
||||
.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())))
|
||||
)
|
||||
.subscribeOn(Schedulers.boundedElastic())
|
||||
.doOnError(throwable -> {
|
||||
log.error("Failed to fetch plugin file from uri.", throwable);
|
||||
throw new ThemeInstallationException("Failed to fetch plugin file from uri.", null,
|
||||
null);
|
||||
})
|
||||
.flatMap(inputStream -> Mono.usingWhen(
|
||||
transferToTemp(inputStream),
|
||||
pluginService::install,
|
||||
this::deleteFileIfExists)
|
||||
)
|
||||
.flatMap(theme -> ServerResponse.ok()
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.bodyValue(theme)
|
||||
);
|
||||
}
|
||||
|
||||
public record InstallFromUriRequest(@Schema(requiredMode = REQUIRED) URI uri) {
|
||||
}
|
||||
|
||||
public record UpgradeFromUriRequest(@Schema(requiredMode = REQUIRED) URI uri) {
|
||||
}
|
||||
|
||||
private Mono<ServerResponse> reload(ServerRequest serverRequest) {
|
||||
var name = serverRequest.pathVariable("name");
|
||||
return ServerResponse.ok().body(pluginService.reload(name), Plugin.class);
|
||||
|
@ -508,4 +602,11 @@ public class PluginEndpoint implements CustomEndpoint {
|
|||
);
|
||||
}
|
||||
|
||||
private Mono<Path> transferToTemp(InputStream inputStream) {
|
||||
return Mono.fromCallable(() -> {
|
||||
Path tempFile = Files.createTempFile("halo-plugins", ".jar");
|
||||
FileCopyUtils.copy(inputStream, Files.newOutputStream(tempFile));
|
||||
return tempFile;
|
||||
}).subscribeOn(Schedulers.boundedElastic());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,6 +36,7 @@ import org.springframework.web.reactive.function.server.ServerResponse;
|
|||
import org.springframework.web.server.ServerWebInputException;
|
||||
import reactor.core.Exceptions;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
import reactor.util.retry.Retry;
|
||||
import run.halo.app.core.extension.Setting;
|
||||
import run.halo.app.core.extension.Theme;
|
||||
|
@ -247,6 +248,7 @@ public class ThemeEndpoint implements CustomEndpoint {
|
|||
.flatMap(upgradeRequest -> Mono.fromCallable(() -> DataBufferUtils.toInputStream(
|
||||
reactiveUrlDataBufferFetcher.fetch(upgradeRequest.uri())))
|
||||
)
|
||||
.subscribeOn(Schedulers.boundedElastic())
|
||||
.doOnError(throwable -> {
|
||||
log.error("Failed to fetch zip file from uri.", throwable);
|
||||
throw new ThemeUpgradeException("Failed to fetch zip file from uri.", null,
|
||||
|
@ -268,6 +270,7 @@ public class ThemeEndpoint implements CustomEndpoint {
|
|||
.flatMap(installRequest -> Mono.fromCallable(() -> DataBufferUtils.toInputStream(
|
||||
reactiveUrlDataBufferFetcher.fetch(installRequest.uri())))
|
||||
)
|
||||
.subscribeOn(Schedulers.boundedElastic())
|
||||
.doOnError(throwable -> {
|
||||
log.error("Failed to fetch zip file from uri.", throwable);
|
||||
throw new ThemeInstallationException("Failed to fetch zip file from uri.", null,
|
||||
|
|
|
@ -16,7 +16,8 @@ rules:
|
|||
resources: [ "plugins" ]
|
||||
verbs: [ "create", "patch", "update", "delete", "deletecollection" ]
|
||||
- apiGroups: [ "api.console.halo.run" ]
|
||||
resources: [ "plugins/upgrade", "plugins/resetconfig", "plugins/config", "plugins/reload" ]
|
||||
resources: [ "plugins/upgrade", "plugins/resetconfig", "plugins/config", "plugins/reload",
|
||||
"plugins/install-from-uri", "plugins/upgrade-from-uri" ]
|
||||
verbs: [ "*" ]
|
||||
- apiGroups: [ "api.console.halo.run" ]
|
||||
resources: [ "plugin-presets" ]
|
||||
|
|
|
@ -40,11 +40,15 @@ import {
|
|||
// @ts-ignore
|
||||
import { ConfigMap } from "../models";
|
||||
// @ts-ignore
|
||||
import { InstallFromUriRequest } from "../models";
|
||||
// @ts-ignore
|
||||
import { Plugin } from "../models";
|
||||
// @ts-ignore
|
||||
import { PluginList } from "../models";
|
||||
// @ts-ignore
|
||||
import { Setting } from "../models";
|
||||
// @ts-ignore
|
||||
import { UpgradeFromUriRequest } from "../models";
|
||||
/**
|
||||
* ApiConsoleHaloRunV1alpha1PluginApi - axios parameter creator
|
||||
* @export
|
||||
|
@ -231,6 +235,67 @@ export const ApiConsoleHaloRunV1alpha1PluginApiAxiosParamCreator = function (
|
|||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Install a plugin from uri.
|
||||
* @param {InstallFromUriRequest} installFromUriRequest
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
installPluginFromUri: async (
|
||||
installFromUriRequest: InstallFromUriRequest,
|
||||
options: AxiosRequestConfig = {}
|
||||
): Promise<RequestArgs> => {
|
||||
// verify required parameter 'installFromUriRequest' is not null or undefined
|
||||
assertParamExists(
|
||||
"installPluginFromUri",
|
||||
"installFromUriRequest",
|
||||
installFromUriRequest
|
||||
);
|
||||
const localVarPath = `/apis/api.console.halo.run/v1alpha1/plugins/-/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 all plugin presets in the system.
|
||||
* @param {*} [options] Override http request option.
|
||||
|
@ -611,6 +676,75 @@ export const ApiConsoleHaloRunV1alpha1PluginApiAxiosParamCreator = function (
|
|||
};
|
||||
localVarRequestOptions.data = localVarFormParams;
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Upgrade a plugin from uri.
|
||||
* @param {string} name
|
||||
* @param {UpgradeFromUriRequest} upgradeFromUriRequest
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
upgradePluginFromUri: async (
|
||||
name: string,
|
||||
upgradeFromUriRequest: UpgradeFromUriRequest,
|
||||
options: AxiosRequestConfig = {}
|
||||
): Promise<RequestArgs> => {
|
||||
// verify required parameter 'name' is not null or undefined
|
||||
assertParamExists("upgradePluginFromUri", "name", name);
|
||||
// verify required parameter 'upgradeFromUriRequest' is not null or undefined
|
||||
assertParamExists(
|
||||
"upgradePluginFromUri",
|
||||
"upgradeFromUriRequest",
|
||||
upgradeFromUriRequest
|
||||
);
|
||||
const localVarPath =
|
||||
`/apis/api.console.halo.run/v1alpha1/plugins/{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,
|
||||
|
@ -700,6 +834,30 @@ export const ApiConsoleHaloRunV1alpha1PluginApiFp = function (
|
|||
configuration
|
||||
);
|
||||
},
|
||||
/**
|
||||
* Install a plugin from uri.
|
||||
* @param {InstallFromUriRequest} installFromUriRequest
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async installPluginFromUri(
|
||||
installFromUriRequest: InstallFromUriRequest,
|
||||
options?: AxiosRequestConfig
|
||||
): Promise<
|
||||
(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Plugin>
|
||||
> {
|
||||
const localVarAxiosArgs =
|
||||
await localVarAxiosParamCreator.installPluginFromUri(
|
||||
installFromUriRequest,
|
||||
options
|
||||
);
|
||||
return createRequestFunction(
|
||||
localVarAxiosArgs,
|
||||
globalAxios,
|
||||
BASE_PATH,
|
||||
configuration
|
||||
);
|
||||
},
|
||||
/**
|
||||
* List all plugin presets in the system.
|
||||
* @param {*} [options] Override http request option.
|
||||
|
@ -863,6 +1021,33 @@ export const ApiConsoleHaloRunV1alpha1PluginApiFp = function (
|
|||
configuration
|
||||
);
|
||||
},
|
||||
/**
|
||||
* Upgrade a plugin from uri.
|
||||
* @param {string} name
|
||||
* @param {UpgradeFromUriRequest} upgradeFromUriRequest
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async upgradePluginFromUri(
|
||||
name: string,
|
||||
upgradeFromUriRequest: UpgradeFromUriRequest,
|
||||
options?: AxiosRequestConfig
|
||||
): Promise<
|
||||
(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Plugin>
|
||||
> {
|
||||
const localVarAxiosArgs =
|
||||
await localVarAxiosParamCreator.upgradePluginFromUri(
|
||||
name,
|
||||
upgradeFromUriRequest,
|
||||
options
|
||||
);
|
||||
return createRequestFunction(
|
||||
localVarAxiosArgs,
|
||||
globalAxios,
|
||||
BASE_PATH,
|
||||
configuration
|
||||
);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -924,6 +1109,20 @@ export const ApiConsoleHaloRunV1alpha1PluginApiFactory = function (
|
|||
)
|
||||
.then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* Install a plugin from uri.
|
||||
* @param {ApiConsoleHaloRunV1alpha1PluginApiInstallPluginFromUriRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
installPluginFromUri(
|
||||
requestParameters: ApiConsoleHaloRunV1alpha1PluginApiInstallPluginFromUriRequest,
|
||||
options?: AxiosRequestConfig
|
||||
): AxiosPromise<Plugin> {
|
||||
return localVarFp
|
||||
.installPluginFromUri(requestParameters.installFromUriRequest, options)
|
||||
.then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* List all plugin presets in the system.
|
||||
* @param {*} [options] Override http request option.
|
||||
|
@ -1025,6 +1224,24 @@ export const ApiConsoleHaloRunV1alpha1PluginApiFactory = function (
|
|||
)
|
||||
.then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* Upgrade a plugin from uri.
|
||||
* @param {ApiConsoleHaloRunV1alpha1PluginApiUpgradePluginFromUriRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
upgradePluginFromUri(
|
||||
requestParameters: ApiConsoleHaloRunV1alpha1PluginApiUpgradePluginFromUriRequest,
|
||||
options?: AxiosRequestConfig
|
||||
): AxiosPromise<Plugin> {
|
||||
return localVarFp
|
||||
.upgradePluginFromUri(
|
||||
requestParameters.name,
|
||||
requestParameters.upgradeFromUriRequest,
|
||||
options
|
||||
)
|
||||
.then((request) => request(axios, basePath));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -1084,6 +1301,20 @@ export interface ApiConsoleHaloRunV1alpha1PluginApiInstallPluginRequest {
|
|||
readonly source?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for installPluginFromUri operation in ApiConsoleHaloRunV1alpha1PluginApi.
|
||||
* @export
|
||||
* @interface ApiConsoleHaloRunV1alpha1PluginApiInstallPluginFromUriRequest
|
||||
*/
|
||||
export interface ApiConsoleHaloRunV1alpha1PluginApiInstallPluginFromUriRequest {
|
||||
/**
|
||||
*
|
||||
* @type {InstallFromUriRequest}
|
||||
* @memberof ApiConsoleHaloRunV1alpha1PluginApiInstallPluginFromUri
|
||||
*/
|
||||
readonly installFromUriRequest: InstallFromUriRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for listPlugins operation in ApiConsoleHaloRunV1alpha1PluginApi.
|
||||
* @export
|
||||
|
@ -1224,6 +1455,27 @@ export interface ApiConsoleHaloRunV1alpha1PluginApiUpgradePluginRequest {
|
|||
readonly source?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for upgradePluginFromUri operation in ApiConsoleHaloRunV1alpha1PluginApi.
|
||||
* @export
|
||||
* @interface ApiConsoleHaloRunV1alpha1PluginApiUpgradePluginFromUriRequest
|
||||
*/
|
||||
export interface ApiConsoleHaloRunV1alpha1PluginApiUpgradePluginFromUriRequest {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ApiConsoleHaloRunV1alpha1PluginApiUpgradePluginFromUri
|
||||
*/
|
||||
readonly name: string;
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {UpgradeFromUriRequest}
|
||||
* @memberof ApiConsoleHaloRunV1alpha1PluginApiUpgradePluginFromUri
|
||||
*/
|
||||
readonly upgradeFromUriRequest: UpgradeFromUriRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* ApiConsoleHaloRunV1alpha1PluginApi - object-oriented interface
|
||||
* @export
|
||||
|
@ -1284,6 +1536,22 @@ export class ApiConsoleHaloRunV1alpha1PluginApi extends BaseAPI {
|
|||
.then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Install a plugin from uri.
|
||||
* @param {ApiConsoleHaloRunV1alpha1PluginApiInstallPluginFromUriRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof ApiConsoleHaloRunV1alpha1PluginApi
|
||||
*/
|
||||
public installPluginFromUri(
|
||||
requestParameters: ApiConsoleHaloRunV1alpha1PluginApiInstallPluginFromUriRequest,
|
||||
options?: AxiosRequestConfig
|
||||
) {
|
||||
return ApiConsoleHaloRunV1alpha1PluginApiFp(this.configuration)
|
||||
.installPluginFromUri(requestParameters.installFromUriRequest, options)
|
||||
.then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* List all plugin presets in the system.
|
||||
* @param {*} [options] Override http request option.
|
||||
|
@ -1394,4 +1662,24 @@ export class ApiConsoleHaloRunV1alpha1PluginApi extends BaseAPI {
|
|||
)
|
||||
.then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Upgrade a plugin from uri.
|
||||
* @param {ApiConsoleHaloRunV1alpha1PluginApiUpgradePluginFromUriRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof ApiConsoleHaloRunV1alpha1PluginApi
|
||||
*/
|
||||
public upgradePluginFromUri(
|
||||
requestParameters: ApiConsoleHaloRunV1alpha1PluginApiUpgradePluginFromUriRequest,
|
||||
options?: AxiosRequestConfig
|
||||
) {
|
||||
return ApiConsoleHaloRunV1alpha1PluginApiFp(this.configuration)
|
||||
.upgradePluginFromUri(
|
||||
requestParameters.name,
|
||||
requestParameters.upgradeFromUriRequest,
|
||||
options
|
||||
)
|
||||
.then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -733,6 +733,9 @@ core:
|
|||
change_status:
|
||||
active_title: Are you sure you want to active this plugin?
|
||||
inactive_title: Are you sure you want to inactive this plugin?
|
||||
remote_download:
|
||||
title: Remote download address detected, do you want to download?
|
||||
description: "Please carefully verify whether this address can be trusted: {url}"
|
||||
filters:
|
||||
status:
|
||||
items:
|
||||
|
@ -750,6 +753,12 @@ core:
|
|||
titles:
|
||||
install: Install plugin
|
||||
upgrade: Upgrade plugin ({display_name})
|
||||
tabs:
|
||||
local: Local
|
||||
remote:
|
||||
title: Remote
|
||||
fields:
|
||||
url: Remote URL
|
||||
operations:
|
||||
active_after_install:
|
||||
title: Install successful
|
||||
|
|
|
@ -733,6 +733,9 @@ core:
|
|||
change_status:
|
||||
active_title: 确定要启用该插件吗?
|
||||
inactive_title: 确定要停用该插件吗?
|
||||
remote_download:
|
||||
title: 检测到了远程下载地址,是否需要下载?
|
||||
description: 请仔细鉴别此地址是否可信:{url}
|
||||
filters:
|
||||
status:
|
||||
items:
|
||||
|
@ -750,6 +753,12 @@ core:
|
|||
titles:
|
||||
install: 安装插件
|
||||
upgrade: 升级插件({display_name})
|
||||
tabs:
|
||||
local: 本地上传
|
||||
remote:
|
||||
title: 远程下载
|
||||
fields:
|
||||
url: 下载地址
|
||||
operations:
|
||||
active_after_install:
|
||||
title: 安装成功
|
||||
|
|
|
@ -733,6 +733,9 @@ core:
|
|||
change_status:
|
||||
active_title: 確定要啟用該插件嗎?
|
||||
inactive_title: 確定要停用該插件嗎?
|
||||
remote_download:
|
||||
title: 偵測到遠端下載地址,是否需要下載?
|
||||
description: 請仔細鑑別此地址是否可信:{url}
|
||||
filters:
|
||||
status:
|
||||
items:
|
||||
|
@ -750,6 +753,12 @@ core:
|
|||
titles:
|
||||
install: 安裝插件
|
||||
upgrade: 升級插件({display_name})
|
||||
tabs:
|
||||
local: 本地上傳
|
||||
remote:
|
||||
title: 遠端下載
|
||||
fields:
|
||||
url: 下載地址
|
||||
operations:
|
||||
active_after_install:
|
||||
title: 安裝成功
|
||||
|
|
|
@ -13,10 +13,11 @@ import {
|
|||
VLoading,
|
||||
VDropdown,
|
||||
VDropdownItem,
|
||||
Dialog,
|
||||
} from "@halo-dev/components";
|
||||
import PluginListItem from "./components/PluginListItem.vue";
|
||||
import PluginUploadModal from "./components/PluginUploadModal.vue";
|
||||
import { computed, ref } from "vue";
|
||||
import { computed, ref, onMounted } from "vue";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import { usePermission } from "@/utils/permission";
|
||||
import FilterTag from "@/components/filter/FilterTag.vue";
|
||||
|
@ -25,6 +26,7 @@ import { getNode } from "@formkit/core";
|
|||
import { useQuery } from "@tanstack/vue-query";
|
||||
import type { Plugin } from "@halo-dev/api-client";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useRouteQuery } from "@vueuse/router";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
|
@ -146,12 +148,34 @@ const { data, isLoading, isFetching, refetch } = useQuery<Plugin[]>({
|
|||
return deletingPlugins?.length ? 3000 : false;
|
||||
},
|
||||
});
|
||||
|
||||
// handle remote download url from route
|
||||
const routeRemoteDownloadUrl = useRouteQuery<string | null>(
|
||||
"remote-download-url"
|
||||
);
|
||||
onMounted(() => {
|
||||
if (routeRemoteDownloadUrl.value) {
|
||||
Dialog.warning({
|
||||
title: t("core.plugin.operations.remote_download.title"),
|
||||
description: t("core.plugin.operations.remote_download.description", {
|
||||
url: routeRemoteDownloadUrl.value,
|
||||
}),
|
||||
confirmText: t("core.common.buttons.download"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
onConfirm() {
|
||||
pluginInstall.value = true;
|
||||
},
|
||||
onCancel() {
|
||||
routeRemoteDownloadUrl.value = null;
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<PluginUploadModal
|
||||
v-if="currentUserHasPermission(['system:plugins:manage'])"
|
||||
v-model:visible="pluginInstall"
|
||||
@close="refetch()"
|
||||
/>
|
||||
|
||||
<VPageHeader :title="$t('core.plugin.title')">
|
||||
|
@ -324,7 +348,7 @@ const { data, isLoading, isFetching, refetch } = useQuery<Plugin[]>({
|
|||
role="list"
|
||||
>
|
||||
<li v-for="plugin in data" :key="plugin.metadata.name">
|
||||
<PluginListItem :plugin="plugin" @reload="refetch()" />
|
||||
<PluginListItem :plugin="plugin" />
|
||||
</li>
|
||||
</ul>
|
||||
</Transition>
|
||||
|
|
|
@ -32,10 +32,6 @@ const props = withDefaults(
|
|||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "reload"): void;
|
||||
}>();
|
||||
|
||||
const { plugin } = toRefs(props);
|
||||
|
||||
const upgradeModal = ref(false);
|
||||
|
@ -43,10 +39,6 @@ const upgradeModal = ref(false);
|
|||
const { getFailedMessage, changeStatus, uninstall } =
|
||||
usePluginLifeCycle(plugin);
|
||||
|
||||
const onUpgradeModalClose = () => {
|
||||
emit("reload");
|
||||
};
|
||||
|
||||
const handleResetSettingConfig = async () => {
|
||||
Dialog.warning({
|
||||
title: t("core.plugin.operations.reset.title"),
|
||||
|
@ -73,11 +65,7 @@ const handleResetSettingConfig = async () => {
|
|||
};
|
||||
</script>
|
||||
<template>
|
||||
<PluginUploadModal
|
||||
v-model:visible="upgradeModal"
|
||||
:upgrade-plugin="plugin"
|
||||
@close="onUpgradeModalClose"
|
||||
/>
|
||||
<PluginUploadModal v-model:visible="upgradeModal" :upgrade-plugin="plugin" />
|
||||
<VEntity>
|
||||
<template #start>
|
||||
<VEntityField>
|
||||
|
|
|
@ -1,14 +1,25 @@
|
|||
<script lang="ts" setup>
|
||||
import { VModal, Dialog, Toast } from "@halo-dev/components";
|
||||
import {
|
||||
VModal,
|
||||
Dialog,
|
||||
Toast,
|
||||
VTabs,
|
||||
VTabItem,
|
||||
VButton,
|
||||
} from "@halo-dev/components";
|
||||
import UppyUpload from "@/components/upload/UppyUpload.vue";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import type { Plugin } from "@halo-dev/api-client";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { computed, ref, watch, nextTick } from "vue";
|
||||
import type { SuccessResponse, ErrorResponse } from "@uppy/core";
|
||||
import type { UppyFile } from "@uppy/utils";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useQueryClient } from "@tanstack/vue-query";
|
||||
import { useRouteQuery } from "@vueuse/router";
|
||||
import { submitForm } from "@formkit/core";
|
||||
|
||||
const { t } = useI18n();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
|
@ -57,8 +68,13 @@ const onUploaded = async (response: SuccessResponse) => {
|
|||
return;
|
||||
}
|
||||
|
||||
const plugin = response.body as Plugin;
|
||||
handleVisibleChange(false);
|
||||
queryClient.invalidateQueries({ queryKey: ["plugins"] });
|
||||
|
||||
handleShowActiveModalAfterInstall(response.body as Plugin);
|
||||
};
|
||||
|
||||
const handleShowActiveModalAfterInstall = (plugin: Plugin) => {
|
||||
Dialog.success({
|
||||
title: t("core.plugin.upload_modal.operations.active_after_install.title"),
|
||||
description: t(
|
||||
|
@ -103,30 +119,49 @@ const PLUGIN_ALREADY_EXISTS_TYPE =
|
|||
|
||||
const onError = (file: UppyFile<unknown>, response: ErrorResponse) => {
|
||||
const body = response.body as PluginInstallationErrorResponse;
|
||||
|
||||
if (body.type === PLUGIN_ALREADY_EXISTS_TYPE) {
|
||||
Dialog.info({
|
||||
title: t(
|
||||
"core.plugin.upload_modal.operations.existed_during_installation.title"
|
||||
),
|
||||
description: t(
|
||||
"core.plugin.upload_modal.operations.existed_during_installation.description"
|
||||
),
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
onConfirm: async () => {
|
||||
await apiClient.plugin.upgradePlugin({
|
||||
name: body.pluginName,
|
||||
file: file.data as File,
|
||||
});
|
||||
|
||||
Toast.success(t("core.common.toast.upgrade_success"));
|
||||
|
||||
window.location.reload();
|
||||
},
|
||||
});
|
||||
handleCatchExistsException(body, file.data as File);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCatchExistsException = async (
|
||||
error: PluginInstallationErrorResponse,
|
||||
file?: File
|
||||
) => {
|
||||
Dialog.info({
|
||||
title: t(
|
||||
"core.plugin.upload_modal.operations.existed_during_installation.title"
|
||||
),
|
||||
description: t(
|
||||
"core.plugin.upload_modal.operations.existed_during_installation.description"
|
||||
),
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
onConfirm: async () => {
|
||||
if (activeTabId.value === "local") {
|
||||
await apiClient.plugin.upgradePlugin({
|
||||
name: error.pluginName,
|
||||
file: file,
|
||||
});
|
||||
} else if (activeTabId.value === "remote") {
|
||||
await apiClient.plugin.upgradePluginFromUri({
|
||||
name: error.pluginName,
|
||||
upgradeFromUriRequest: {
|
||||
uri: remoteDownloadUrl.value,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
throw new Error("Unknown tab id");
|
||||
}
|
||||
|
||||
Toast.success(t("core.common.toast.upgrade_success"));
|
||||
|
||||
window.location.reload();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(newValue) => {
|
||||
|
@ -140,24 +175,120 @@ watch(
|
|||
}
|
||||
}
|
||||
);
|
||||
|
||||
// remote download
|
||||
const activeTabId = ref("local");
|
||||
const remoteDownloadUrl = ref("");
|
||||
const downloading = ref(false);
|
||||
|
||||
const handleDownloadPlugin = async () => {
|
||||
try {
|
||||
downloading.value = true;
|
||||
if (props.upgradePlugin) {
|
||||
await apiClient.plugin.upgradePluginFromUri({
|
||||
name: props.upgradePlugin.metadata.name,
|
||||
upgradeFromUriRequest: {
|
||||
uri: remoteDownloadUrl.value,
|
||||
},
|
||||
});
|
||||
|
||||
Toast.success(t("core.common.toast.upgrade_success"));
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
const { data: plugin } = await apiClient.plugin.installPluginFromUri({
|
||||
installFromUriRequest: {
|
||||
uri: remoteDownloadUrl.value,
|
||||
},
|
||||
});
|
||||
|
||||
handleVisibleChange(false);
|
||||
queryClient.invalidateQueries({ queryKey: ["plugins"] });
|
||||
|
||||
handleShowActiveModalAfterInstall(plugin);
|
||||
|
||||
// eslint-disable-next-line
|
||||
} catch (error: any) {
|
||||
const data = error?.response.data as PluginInstallationErrorResponse;
|
||||
if (data?.type === PLUGIN_ALREADY_EXISTS_TYPE) {
|
||||
handleCatchExistsException(data);
|
||||
}
|
||||
|
||||
console.error("Failed to download plugin", error);
|
||||
} finally {
|
||||
routeRemoteDownloadUrl.value = null;
|
||||
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;
|
||||
nextTick(() => {
|
||||
submitForm("plugin-remote-download-form");
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<VModal
|
||||
:visible="visible"
|
||||
:width="600"
|
||||
:title="modalTitle"
|
||||
:centered="false"
|
||||
@update:visible="handleVisibleChange"
|
||||
>
|
||||
<UppyUpload
|
||||
v-if="uploadVisible"
|
||||
:restrictions="{
|
||||
maxNumberOfFiles: 1,
|
||||
allowedFileTypes: ['.jar'],
|
||||
}"
|
||||
:endpoint="endpoint"
|
||||
auto-proceed
|
||||
@uploaded="onUploaded"
|
||||
@error="onError"
|
||||
/>
|
||||
<VTabs v-model:active-id="activeTabId" type="outline" class="!rounded-none">
|
||||
<VTabItem id="local" :label="$t('core.plugin.upload_modal.tabs.local')">
|
||||
<UppyUpload
|
||||
v-if="uploadVisible"
|
||||
:restrictions="{
|
||||
maxNumberOfFiles: 1,
|
||||
allowedFileTypes: ['.jar'],
|
||||
}"
|
||||
:endpoint="endpoint"
|
||||
auto-proceed
|
||||
@uploaded="onUploaded"
|
||||
@error="onError"
|
||||
/>
|
||||
</VTabItem>
|
||||
<VTabItem
|
||||
id="remote"
|
||||
:label="$t('core.plugin.upload_modal.tabs.remote.title')"
|
||||
>
|
||||
<FormKit
|
||||
id="plugin-remote-download-form"
|
||||
name="plugin-remote-download-form"
|
||||
type="form"
|
||||
:preserve="true"
|
||||
@submit="handleDownloadPlugin"
|
||||
>
|
||||
<FormKit
|
||||
v-model="remoteDownloadUrl"
|
||||
:label="$t('core.plugin.upload_modal.tabs.remote.fields.url')"
|
||||
type="text"
|
||||
></FormKit>
|
||||
</FormKit>
|
||||
|
||||
<div class="pt-5">
|
||||
<VButton
|
||||
:loading="downloading"
|
||||
type="secondary"
|
||||
@click="$formkit.submit('plugin-remote-download-form')"
|
||||
>
|
||||
{{ $t("core.common.buttons.download") }}
|
||||
</VButton>
|
||||
</div>
|
||||
</VTabItem>
|
||||
</VTabs>
|
||||
</VModal>
|
||||
</template>
|
||||
|
|
Loading…
Reference in New Issue