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;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import lombok.Data;
|
||||
|
@ -13,6 +14,8 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
|
|||
@ConfigurationProperties(prefix = "halo")
|
||||
public class HaloProperties {
|
||||
|
||||
private Path workDir;
|
||||
|
||||
private Set<String> initialExtensionLocations = new HashSet<>();
|
||||
|
||||
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
|
||||
* absolute directory address.
|
||||
*/
|
||||
private String pluginsRoot = "plugins";
|
||||
private String pluginsRoot;
|
||||
|
||||
/**
|
||||
* Allows providing custom plugin loaders.
|
||||
|
|
|
@ -1,32 +1,13 @@
|
|||
server:
|
||||
port: 8090
|
||||
|
||||
spring:
|
||||
output:
|
||||
ansi:
|
||||
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:
|
||||
hibernate:
|
||||
ddl-auto: update
|
||||
open-in-view: false
|
||||
show-sql: true
|
||||
|
||||
security:
|
||||
oauth2:
|
||||
client:
|
||||
registration:
|
||||
google:
|
||||
client-id: google-client-id
|
||||
client-secret: google-client-secret
|
||||
|
||||
halo:
|
||||
security:
|
||||
oauth2:
|
||||
|
@ -41,7 +22,6 @@ halo:
|
|||
- "build/resources"
|
||||
lib-directories:
|
||||
- "libs"
|
||||
plugins-root: plugins
|
||||
logging:
|
||||
level:
|
||||
run.halo.app: DEBUG
|
||||
|
|
|
@ -9,7 +9,7 @@ spring:
|
|||
|
||||
# H2 database configuration.
|
||||
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
|
||||
password: 123456
|
||||
|
||||
|
@ -35,6 +35,9 @@ halo:
|
|||
jwt:
|
||||
public-key-location: classpath:app.pub
|
||||
private-key-location: classpath:app.key
|
||||
work-dir: ${user.home}/halo-next
|
||||
plugin:
|
||||
plugins-root: ${halo.work-dir}/plugins
|
||||
|
||||
springdoc:
|
||||
api-docs:
|
||||
|
|
Loading…
Reference in New Issue