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
guqing 2022-10-25 10:42:11 +08:00 committed by GitHub
parent 7b4dd59c58
commit 0a46ec8123
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 122 additions and 20 deletions

View File

@ -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());
}
}

View File

@ -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;

View File

@ -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);
}
}
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}