mirror of https://github.com/halo-dev/halo
refactor: files were not deleted when the plugin was uninstalled (#2613)
#### What type of PR is this? /kind improvement /area core /milestone 2.0 #### What this PR does / why we need it: 修复插件卸载时没有连同删除插件 JAR 文件的问题 #### Which issue(s) this PR fixes: Fixes #2552 #### Special notes for your reviewer: how to test it? 1. 以 deployment 模式安装一个插件 2. 插件的名称在插件目录是以 {pluginName}-{version}.jar 的方式命名的 3. 卸载插件时会连同删除插件的 JAR 文件 4. 开发模式下不会删除插件文件 /cc @halo-dev/sig-halo #### Does this PR introduce a user-facing change? ```release-note 修复插件卸载时没有连同删除插件 JAR 文件的问题 ```pull/2625/head
parent
7b4dd59c58
commit
0a46ec8123
|
@ -53,6 +53,13 @@ public class Plugin extends AbstractExtension {
|
|||
|
||||
private String displayName;
|
||||
|
||||
/**
|
||||
* @see <a href="semver.org">semantic version</a>
|
||||
*/
|
||||
@Schema(required = true, pattern = "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-("
|
||||
+ "(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\."
|
||||
+ "(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\"
|
||||
+ ".[0-9a-zA-Z-]+)*))?$")
|
||||
private String version;
|
||||
|
||||
private String author;
|
||||
|
@ -106,4 +113,9 @@ public class Plugin extends AbstractExtension {
|
|||
|
||||
private String stylesheet;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public String generateFileName() {
|
||||
return String.format("%s-%s.jar", getMetadata().getName(), spec.getVersion());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,9 +36,11 @@ import org.springframework.web.reactive.function.server.ServerResponse;
|
|||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.ServerWebInputException;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
import run.halo.app.core.extension.Plugin;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.extension.router.IListRequest.QueryListRequest;
|
||||
import run.halo.app.infra.utils.FileUtils;
|
||||
import run.halo.app.plugin.PluginProperties;
|
||||
import run.halo.app.plugin.YamlPluginFinder;
|
||||
|
||||
|
@ -184,25 +186,51 @@ public class PluginEndpoint implements CustomEndpoint {
|
|||
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);
|
||||
})
|
||||
.flatMap(pluginPath -> {
|
||||
log.info("Plugin uploaded at {}", pluginPath);
|
||||
var plugin = new YamlPluginFinder().find(pluginPath);
|
||||
// overwrite the enabled flag
|
||||
plugin.getSpec().setEnabled(false);
|
||||
.flatMap(this::transferToTemp)
|
||||
.flatMap(tempJarFilePath -> {
|
||||
var plugin = new YamlPluginFinder().find(tempJarFilePath);
|
||||
return client.fetch(Plugin.class, plugin.getMetadata().getName())
|
||||
.switchIfEmpty(Mono.defer(() -> client.create(plugin)));
|
||||
.switchIfEmpty(Mono.defer(() -> client.create(plugin)))
|
||||
.publishOn(Schedulers.boundedElastic())
|
||||
.map(created -> {
|
||||
String fileName = created.generateFileName();
|
||||
var pluginRoot = Paths.get(pluginProperties.getPluginsRoot());
|
||||
createDirectoriesIfNotExists(pluginRoot);
|
||||
Path pluginFilePath = pluginRoot.resolve(fileName);
|
||||
if (Files.exists(pluginFilePath)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Plugin already installed : " + pluginFilePath);
|
||||
}
|
||||
FileUtils.copy(tempJarFilePath, pluginFilePath);
|
||||
return created;
|
||||
})
|
||||
.doOnError(error -> {
|
||||
log.error("Failed to install plugin", error);
|
||||
client.fetch(Plugin.class, plugin.getMetadata().getName())
|
||||
.map(client::delete)
|
||||
.subscribe();
|
||||
})
|
||||
.doFinally(signalType -> {
|
||||
try {
|
||||
Files.deleteIfExists(tempJarFilePath);
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to delete temp file: {}", tempJarFilePath, e);
|
||||
}
|
||||
});
|
||||
})
|
||||
.flatMap(plugin -> ServerResponse.ok()
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.bodyValue(plugin));
|
||||
}
|
||||
|
||||
private Mono<Path> transferToTemp(FilePart filePart) {
|
||||
return Mono.fromCallable(() -> Files.createTempFile("halo-plugins", ".jar"))
|
||||
.subscribeOn(Schedulers.boundedElastic())
|
||||
.flatMap(path -> filePart.transferTo(path)
|
||||
.thenReturn(path)
|
||||
);
|
||||
}
|
||||
|
||||
void createDirectoriesIfNotExists(Path directory) {
|
||||
if (Files.exists(directory)) {
|
||||
return;
|
||||
|
|
|
@ -1,15 +1,20 @@
|
|||
package run.halo.app.core.extension.reconciler;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.pf4j.PluginRuntimeException;
|
||||
import org.pf4j.PluginState;
|
||||
import org.pf4j.PluginWrapper;
|
||||
import org.pf4j.RuntimeMode;
|
||||
import run.halo.app.core.extension.Plugin;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.controller.Reconciler;
|
||||
|
@ -27,7 +32,7 @@ import run.halo.app.plugin.resources.BundleResourceUtils;
|
|||
*/
|
||||
@Slf4j
|
||||
public class PluginReconciler implements Reconciler<Request> {
|
||||
|
||||
private static final String FINALIZER_NAME = "plugin-protection";
|
||||
private final ExtensionClient client;
|
||||
private final HaloPluginManager haloPluginManager;
|
||||
|
||||
|
@ -41,7 +46,13 @@ public class PluginReconciler implements Reconciler<Request> {
|
|||
public Result reconcile(Request request) {
|
||||
return client.fetch(Plugin.class, request.name())
|
||||
.map(plugin -> {
|
||||
final Plugin oldPlugin = deepCopy(plugin);
|
||||
if (plugin.getMetadata().getDeletionTimestamp() != null) {
|
||||
cleanUpResourcesAndRemoveFinalizer(request.name());
|
||||
return new Result(false, null);
|
||||
}
|
||||
addFinalizerIfNecessary(plugin);
|
||||
|
||||
final Plugin oldPlugin = JsonUtils.deepCopy(plugin);
|
||||
try {
|
||||
reconcilePluginState(plugin);
|
||||
// TODO: reconcile other plugin resources
|
||||
|
@ -96,10 +107,6 @@ public class PluginReconciler implements Reconciler<Request> {
|
|||
}
|
||||
}
|
||||
|
||||
private Plugin deepCopy(Plugin plugin) {
|
||||
return JsonUtils.jsonToObject(JsonUtils.objectToJson(plugin), Plugin.class);
|
||||
}
|
||||
|
||||
private void ensurePluginLoaded() {
|
||||
// load plugin if exists in plugin root paths.
|
||||
List<PluginWrapper> loadedPlugins = haloPluginManager.getPlugins();
|
||||
|
@ -161,4 +168,50 @@ public class PluginReconciler implements Reconciler<Request> {
|
|||
throw new PluginRuntimeException(startingError.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void addFinalizerIfNecessary(Plugin oldPlugin) {
|
||||
Set<String> finalizers = oldPlugin.getMetadata().getFinalizers();
|
||||
if (finalizers != null && finalizers.contains(FINALIZER_NAME)) {
|
||||
return;
|
||||
}
|
||||
client.fetch(Plugin.class, oldPlugin.getMetadata().getName())
|
||||
.ifPresent(plugin -> {
|
||||
Set<String> newFinalizers = plugin.getMetadata().getFinalizers();
|
||||
if (newFinalizers == null) {
|
||||
newFinalizers = new HashSet<>();
|
||||
plugin.getMetadata().setFinalizers(newFinalizers);
|
||||
}
|
||||
newFinalizers.add(FINALIZER_NAME);
|
||||
client.update(plugin);
|
||||
});
|
||||
}
|
||||
|
||||
private void cleanUpResourcesAndRemoveFinalizer(String pluginName) {
|
||||
client.fetch(Plugin.class, pluginName).ifPresent(plugin -> {
|
||||
cleanUpResources(plugin);
|
||||
if (plugin.getMetadata().getFinalizers() != null) {
|
||||
plugin.getMetadata().getFinalizers().remove(FINALIZER_NAME);
|
||||
}
|
||||
client.update(plugin);
|
||||
});
|
||||
}
|
||||
|
||||
private void cleanUpResources(Plugin plugin) {
|
||||
PluginWrapper pluginWrapper = haloPluginManager.getPlugin(plugin.getMetadata().getName());
|
||||
if (pluginWrapper == null) {
|
||||
return;
|
||||
}
|
||||
// stop and unload plugin, see also PluginBeforeStopSyncListener
|
||||
haloPluginManager.stopPlugin(pluginWrapper.getPluginId());
|
||||
haloPluginManager.unloadPlugin(pluginWrapper.getPluginId());
|
||||
// delete plugin resources
|
||||
if (RuntimeMode.DEPLOYMENT.equals(pluginWrapper.getRuntimeMode())) {
|
||||
// delete plugin file
|
||||
try {
|
||||
Files.deleteIfExists(pluginWrapper.getPluginPath());
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import static org.springframework.util.FileSystemUtils.deleteRecursively;
|
|||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.CopyOption;
|
||||
import java.nio.file.DirectoryNotEmptyException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
@ -194,4 +195,12 @@ public abstract class FileUtils {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void copy(Path source, Path dest, CopyOption... options) {
|
||||
try {
|
||||
Files.copy(source, dest, options);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -189,7 +189,7 @@ class PluginReconcilerTest {
|
|||
assertThat(result).isNotNull();
|
||||
assertThat(result.reEnqueue()).isEqualTo(true);
|
||||
|
||||
verify(extensionClient, times(1)).update(any());
|
||||
verify(extensionClient, times(2)).update(any());
|
||||
return pluginCaptor;
|
||||
}
|
||||
|
||||
|
@ -202,7 +202,7 @@ class PluginReconcilerTest {
|
|||
assertThat(result).isNotNull();
|
||||
assertThat(result.reEnqueue()).isEqualTo(false);
|
||||
|
||||
verify(extensionClient, times(1)).update(any());
|
||||
verify(extensionClient, times(2)).update(any());
|
||||
return pluginCaptor;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue