mirror of https://github.com/halo-dev/halo
Provide an endpoint to install plugin using Jar file (#2271)
#### What type of PR is this? /kind feature /area core /milestone 2.0 #### What this PR does / why we need it: Provide an endpoint to install plugin using Jar file. #### Special notes for your reviewer: Currently, you could login and open the swagger ui to test against this feature. #### Does this PR introduce a user-facing change? ```release-note None ```pull/2272/head
parent
4cb94d3752
commit
0b4b1c321b
|
@ -0,0 +1,117 @@
|
||||||
|
package run.halo.app.core.extension.endpoint;
|
||||||
|
|
||||||
|
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.requestbody.Builder.requestBodyBuilder;
|
||||||
|
import static org.springframework.web.reactive.function.server.RequestPredicates.contentType;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springdoc.core.fn.builders.schema.Builder;
|
||||||
|
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder;
|
||||||
|
import org.springframework.core.ParameterizedTypeReference;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.codec.multipart.FilePart;
|
||||||
|
import org.springframework.http.codec.multipart.Part;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.util.MultiValueMap;
|
||||||
|
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||||
|
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||||
|
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
|
import org.springframework.web.server.ServerWebInputException;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import run.halo.app.core.extension.Plugin;
|
||||||
|
import run.halo.app.extension.ExtensionClient;
|
||||||
|
import run.halo.app.plugin.PluginProperties;
|
||||||
|
import run.halo.app.plugin.YamlPluginFinder;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class PluginEndpoint implements CustomEndpoint {
|
||||||
|
|
||||||
|
private final PluginProperties pluginProperties;
|
||||||
|
|
||||||
|
private final ExtensionClient client;
|
||||||
|
|
||||||
|
public PluginEndpoint(PluginProperties pluginProperties, ExtensionClient client) {
|
||||||
|
this.pluginProperties = pluginProperties;
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RouterFunction<ServerResponse> endpoint() {
|
||||||
|
final var tag = "api.halo.run/v1alpha1/Plugin";
|
||||||
|
return SpringdocRouteBuilder.route()
|
||||||
|
.POST("plugins/install", contentType(MediaType.MULTIPART_FORM_DATA),
|
||||||
|
this::install, builder -> builder.operationId("InstallPlugin")
|
||||||
|
.description("Install a plugin by uploading a Jar file.")
|
||||||
|
.tag(tag)
|
||||||
|
.requestBody(requestBodyBuilder()
|
||||||
|
.required(true)
|
||||||
|
.content(contentBuilder()
|
||||||
|
.mediaType(MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||||
|
.schema(Builder.schemaBuilder().implementation(InstallRequest.class))
|
||||||
|
))
|
||||||
|
.response(responseBuilder())
|
||||||
|
)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public record InstallRequest(
|
||||||
|
@Schema(required = true, description = "Plugin Jar file.") FilePart file) {
|
||||||
|
}
|
||||||
|
|
||||||
|
Mono<ServerResponse> install(ServerRequest request) {
|
||||||
|
return request.bodyToMono(new ParameterizedTypeReference<MultiValueMap<String, Part>>() {
|
||||||
|
})
|
||||||
|
.flatMap(this::getJarFilePart)
|
||||||
|
.flatMap(file -> {
|
||||||
|
var pluginRoot = Paths.get(pluginProperties.getPluginsRoot());
|
||||||
|
createDirectoriesIfNotExists(pluginRoot);
|
||||||
|
var pluginPath = pluginRoot.resolve(file.filename());
|
||||||
|
return file.transferTo(pluginPath).thenReturn(pluginPath);
|
||||||
|
}).map(pluginPath -> {
|
||||||
|
log.info("Plugin uploaded at {}", pluginPath);
|
||||||
|
var plugin = new YamlPluginFinder().find(pluginPath);
|
||||||
|
// overwrite the enabled flag
|
||||||
|
plugin.getSpec().setEnabled(false);
|
||||||
|
var createdPlugin =
|
||||||
|
client.fetch(Plugin.class, plugin.getMetadata().getName()).orElseGet(() -> {
|
||||||
|
client.create(plugin);
|
||||||
|
return client.fetch(Plugin.class, plugin.getMetadata().getName())
|
||||||
|
.orElseThrow();
|
||||||
|
});
|
||||||
|
return createdPlugin;
|
||||||
|
}).flatMap(plugin -> ServerResponse.ok()
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.bodyValue(plugin));
|
||||||
|
}
|
||||||
|
|
||||||
|
void createDirectoriesIfNotExists(Path directory) {
|
||||||
|
if (Files.exists(directory)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Files.createDirectories(directory);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("Failed to create directory " + directory, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Mono<FilePart> getJarFilePart(MultiValueMap<String, Part> formData) {
|
||||||
|
Part part = formData.getFirst("file");
|
||||||
|
if (!(part instanceof FilePart file)) {
|
||||||
|
return Mono.error(new ServerWebInputException(
|
||||||
|
"Invalid parameter of file, binary data is required"));
|
||||||
|
}
|
||||||
|
if (!Paths.get(file.filename()).toString().endsWith(".jar")) {
|
||||||
|
return Mono.error(new ServerWebInputException(
|
||||||
|
"Invalid file type, only jar is supported"));
|
||||||
|
}
|
||||||
|
return Mono.just(file);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
package run.halo.app.infra.properties;
|
package run.halo.app.infra.properties;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
@ -13,6 +14,8 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
@ConfigurationProperties(prefix = "halo")
|
@ConfigurationProperties(prefix = "halo")
|
||||||
public class HaloProperties {
|
public class HaloProperties {
|
||||||
|
|
||||||
|
private Path workDir;
|
||||||
|
|
||||||
private Set<String> initialExtensionLocations = new HashSet<>();
|
private Set<String> initialExtensionLocations = new HashSet<>();
|
||||||
|
|
||||||
private final ExtensionProperties extension = new ExtensionProperties();
|
private final ExtensionProperties extension = new ExtensionProperties();
|
||||||
|
|
|
@ -57,7 +57,7 @@ public class PluginProperties {
|
||||||
* Plugin root directory: default “plugins”; when non-jar mode plugin, the value should be an
|
* Plugin root directory: default “plugins”; when non-jar mode plugin, the value should be an
|
||||||
* absolute directory address.
|
* absolute directory address.
|
||||||
*/
|
*/
|
||||||
private String pluginsRoot = "plugins";
|
private String pluginsRoot;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allows providing custom plugin loaders.
|
* Allows providing custom plugin loaders.
|
||||||
|
|
|
@ -1,32 +1,13 @@
|
||||||
server:
|
server:
|
||||||
port: 8090
|
port: 8090
|
||||||
|
|
||||||
spring:
|
spring:
|
||||||
output:
|
output:
|
||||||
ansi:
|
ansi:
|
||||||
enabled: always
|
enabled: always
|
||||||
datasource:
|
|
||||||
type: com.zaxxer.hikari.HikariDataSource
|
|
||||||
|
|
||||||
# H2 database configuration.
|
|
||||||
driver-class-name: org.h2.Driver
|
|
||||||
url: jdbc:h2:file:~/halo-next/db/halo;AUTO_SERVER=TRUE
|
|
||||||
username: admin
|
|
||||||
password: 123456
|
|
||||||
|
|
||||||
jpa:
|
jpa:
|
||||||
hibernate:
|
|
||||||
ddl-auto: update
|
|
||||||
open-in-view: false
|
|
||||||
show-sql: true
|
show-sql: true
|
||||||
|
|
||||||
security:
|
|
||||||
oauth2:
|
|
||||||
client:
|
|
||||||
registration:
|
|
||||||
google:
|
|
||||||
client-id: google-client-id
|
|
||||||
client-secret: google-client-secret
|
|
||||||
|
|
||||||
halo:
|
halo:
|
||||||
security:
|
security:
|
||||||
oauth2:
|
oauth2:
|
||||||
|
@ -41,7 +22,6 @@ halo:
|
||||||
- "build/resources"
|
- "build/resources"
|
||||||
lib-directories:
|
lib-directories:
|
||||||
- "libs"
|
- "libs"
|
||||||
plugins-root: plugins
|
|
||||||
logging:
|
logging:
|
||||||
level:
|
level:
|
||||||
run.halo.app: DEBUG
|
run.halo.app: DEBUG
|
||||||
|
|
|
@ -9,7 +9,7 @@ spring:
|
||||||
|
|
||||||
# H2 database configuration.
|
# H2 database configuration.
|
||||||
driver-class-name: org.h2.Driver
|
driver-class-name: org.h2.Driver
|
||||||
url: jdbc:h2:file:~/halo-next/db/halo;AUTO_SERVER=TRUE
|
url: jdbc:h2:file:${halo.work-dir}/db/halo;AUTO_SERVER=TRUE
|
||||||
username: admin
|
username: admin
|
||||||
password: 123456
|
password: 123456
|
||||||
|
|
||||||
|
@ -35,6 +35,9 @@ halo:
|
||||||
jwt:
|
jwt:
|
||||||
public-key-location: classpath:app.pub
|
public-key-location: classpath:app.pub
|
||||||
private-key-location: classpath:app.key
|
private-key-location: classpath:app.key
|
||||||
|
work-dir: ${user.home}/halo-next
|
||||||
|
plugin:
|
||||||
|
plugins-root: ${halo.work-dir}/plugins
|
||||||
|
|
||||||
springdoc:
|
springdoc:
|
||||||
api-docs:
|
api-docs:
|
||||||
|
|
Loading…
Reference in New Issue