Support obtaining plugins root in plugins (#6269)

#### What type of PR is this?

/kind feature
/kind api-change
/area core
/area plugin

#### What this PR does / why we need it:

This PR supports obtaining plugins root in plugins. Below is an example in plugin:

```java
@Component
class PluginsRootGetterDemo {

    private final PluginsRootGetter pluginsRootGetter;

    PluginsRootGetterDemo(PluginsRootGetter pluginsRootGetter) {
        this.pluginsRootGetter = pluginsRootGetter;
    }

}
```

Meanwhile, I remove the `PluginProperties#pluginsRoot` for a clear way to obtain plugins root.

#### Does this PR introduce a user-facing change?

```release-note
支持在插件中获取插件根目录
```
pull/6271/head
John Niang 2024-07-04 21:36:33 +08:00 committed by GitHub
parent 36fb44c8b7
commit ad66247872
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 101 additions and 38 deletions

View File

@ -0,0 +1,14 @@
package run.halo.app.plugin;
import java.nio.file.Path;
import java.util.function.Supplier;
/**
* An interface to get the root path of plugins.
*
* @author johnniang
* @since 2.18.0
*/
public interface PluginsRootGetter extends Supplier<Path> {
}

View File

@ -13,7 +13,6 @@ import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Clock;
import java.time.Duration;
import java.util.ArrayList;
@ -61,8 +60,8 @@ import run.halo.app.infra.exception.UnsatisfiedAttributeValueException;
import run.halo.app.infra.utils.FileUtils;
import run.halo.app.infra.utils.VersionUtils;
import run.halo.app.plugin.PluginConst;
import run.halo.app.plugin.PluginProperties;
import run.halo.app.plugin.PluginUtils;
import run.halo.app.plugin.PluginsRootGetter;
import run.halo.app.plugin.SpringPluginManager;
import run.halo.app.plugin.YamlPluginDescriptorFinder;
import run.halo.app.plugin.YamlPluginFinder;
@ -79,7 +78,7 @@ public class PluginServiceImpl implements PluginService, InitializingBean, Dispo
private final SystemVersionSupplier systemVersion;
private final PluginProperties pluginProperties;
private final PluginsRootGetter pluginsRootGetter;
private final SpringPluginManager pluginManager;
@ -93,11 +92,13 @@ public class PluginServiceImpl implements PluginService, InitializingBean, Dispo
private Clock clock = Clock.systemUTC();
public PluginServiceImpl(ReactiveExtensionClient client, SystemVersionSupplier systemVersion,
PluginProperties pluginProperties, SpringPluginManager pluginManager) {
public PluginServiceImpl(ReactiveExtensionClient client,
SystemVersionSupplier systemVersion,
PluginsRootGetter pluginsRootGetter,
SpringPluginManager pluginManager) {
this.client = client;
this.systemVersion = systemVersion;
this.pluginProperties = pluginProperties;
this.pluginsRootGetter = pluginsRootGetter;
this.pluginManager = pluginManager;
this.jsBundleCache = new BundleCache(".js");
@ -424,7 +425,7 @@ public class PluginServiceImpl implements PluginService, InitializingBean, Dispo
return Mono.fromCallable(
() -> {
var fileName = PluginUtils.generateFileName(plugin);
var pluginRoot = Paths.get(pluginProperties.getPluginsRoot());
var pluginRoot = pluginsRootGetter.get();
try {
Files.createDirectories(pluginRoot);
} catch (IOException e) {

View File

@ -1,12 +1,10 @@
package run.halo.app.plugin;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.pf4j.CompoundPluginLoader;
import org.pf4j.CompoundPluginRepository;
import org.pf4j.DefaultPluginManager;
@ -46,13 +44,16 @@ public class HaloPluginManager extends DefaultPluginManager implements SpringPlu
private final PluginProperties pluginProperties;
private final PluginsRootGetter pluginsRootGetter;
public HaloPluginManager(ApplicationContext rootContext,
PluginProperties pluginProperties,
SystemVersionSupplier systemVersionSupplier) {
SystemVersionSupplier systemVersionSupplier, PluginsRootGetter pluginsRootGetter) {
this.pluginProperties = pluginProperties;
this.rootContext = rootContext;
// We have to initialize share context lazily because the root context has not refreshed
this.sharedContext = Lazy.of(() -> SharedApplicationContextFactory.create(rootContext));
this.pluginsRootGetter = pluginsRootGetter;
super.runtimeMode = pluginProperties.getRuntimeMode();
setExactVersionAllowed(pluginProperties.isExactVersionAllowed());
@ -124,11 +125,7 @@ public class HaloPluginManager extends DefaultPluginManager implements SpringPlu
@Override
protected List<Path> createPluginsRoot() {
var pluginsRoot = pluginProperties.getPluginsRoot();
if (StringUtils.isNotBlank(pluginsRoot)) {
return List.of(Paths.get(pluginsRoot));
}
return super.createPluginsRoot();
return List.of(pluginsRootGetter.get());
}
@Override

View File

@ -45,8 +45,11 @@ public class PluginAutoConfiguration {
@Bean
public SpringPluginManager pluginManager(ApplicationContext context,
SystemVersionSupplier systemVersionSupplier,
PluginProperties pluginProperties) {
return new HaloPluginManager(context, pluginProperties, systemVersionSupplier);
PluginProperties pluginProperties,
PluginsRootGetter pluginsRootGetter) {
return new HaloPluginManager(
context, pluginProperties, systemVersionSupplier, pluginsRootGetter
);
}
@Bean

View File

@ -60,10 +60,4 @@ public class PluginProperties {
*/
private RuntimeMode runtimeMode = RuntimeMode.DEPLOYMENT;
/**
* Plugin root directory: default plugins; when non-jar mode plugin, the value should be an
* absolute directory address.
*/
private String pluginsRoot;
}

View File

@ -0,0 +1,28 @@
package run.halo.app.plugin;
import java.nio.file.Path;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import run.halo.app.infra.properties.HaloProperties;
/**
* Default implementation of {@link PluginsRootGetter}.
*
* @author johnniang
*/
@Component
public class PluginsRootGetterImpl implements PluginsRootGetter {
private final HaloProperties haloProperties;
public PluginsRootGetterImpl(HaloProperties haloProperties) {
this.haloProperties = haloProperties;
}
@Override
@NonNull
public Path get() {
return haloProperties.getWorkDir().resolve("plugins");
}
}

View File

@ -62,6 +62,10 @@ public enum SharedApplicationContextFactory {
rootContext.getBean(CacheManager.class));
beanFactory.registerSingleton("loginHandlerEnhancer",
rootContext.getBean(LoginHandlerEnhancer.class));
rootContext.getBeanProvider(PluginsRootGetter.class)
.ifUnique(pluginsRootGetter ->
beanFactory.registerSingleton("pluginsRootGetter", pluginsRootGetter)
);
// TODO add more shared instance here
sharedContext.refresh();

View File

@ -34,8 +34,6 @@ spring:
halo:
work-dir: ${user.home}/.halo2
plugin:
plugins-root: ${halo.work-dir}/plugins
attachment:
resource-mappings:
- pathPattern: /upload/**

View File

@ -55,15 +55,11 @@ import run.halo.app.extension.Metadata;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.infra.SystemVersionSupplier;
import run.halo.app.infra.utils.FileUtils;
import run.halo.app.plugin.PluginProperties;
@Slf4j
@ExtendWith(MockitoExtension.class)
class PluginEndpointTest {
@Mock
PluginProperties pluginProperties;
@Mock
private ReactiveExtensionClient client;
@ -221,8 +217,6 @@ class PluginEndpointTest {
lenient().when(systemVersionSupplier.get()).thenReturn(Version.valueOf("0.0.0"));
tempDirectory = Files.createTempDirectory("halo-test-plugin-upgrade-");
lenient().when(pluginProperties.getPluginsRoot())
.thenReturn(tempDirectory.resolve("plugins").toString());
plugin002 = tempDirectory.resolve("plugin-0.0.2.jar");
var plugin002Uri = requireNonNull(

View File

@ -58,7 +58,7 @@ import run.halo.app.infra.SystemVersionSupplier;
import run.halo.app.infra.exception.PluginAlreadyExistsException;
import run.halo.app.infra.utils.FileUtils;
import run.halo.app.plugin.PluginConst;
import run.halo.app.plugin.PluginProperties;
import run.halo.app.plugin.PluginsRootGetter;
import run.halo.app.plugin.SpringPluginManager;
import run.halo.app.plugin.YamlPluginFinder;
@ -72,7 +72,7 @@ class PluginServiceImplTest {
ReactiveExtensionClient client;
@Mock
PluginProperties pluginProperties;
PluginsRootGetter pluginsRootGetter;
@Mock
SpringPluginManager pluginManager;
@ -123,9 +123,6 @@ class PluginServiceImplTest {
getClass().getClassLoader().getResource("plugin/plugin-0.0.2")).toURI();
FileUtils.jar(Paths.get(fakePluingUri), tempDirectory.resolve("plugin-0.0.2.jar"));
lenient().when(pluginProperties.getPluginsRoot())
.thenReturn(tempDirectory.resolve("plugins").toString());
lenient().when(systemVersionSupplier.get()).thenReturn(Version.valueOf("0.0.0"));
}
@ -144,6 +141,7 @@ class PluginServiceImplTest {
@Test
void installWhenPluginNotExist() {
when(pluginsRootGetter.get()).thenReturn(tempDirectory.resolve("plugins"));
when(client.fetch(Plugin.class, "fake-plugin")).thenReturn(Mono.empty());
var createdPlugin = mock(Plugin.class);
when(client.create(isA(Plugin.class))).thenReturn(Mono.just(createdPlugin));
@ -154,7 +152,6 @@ class PluginServiceImplTest {
verify(client).fetch(Plugin.class, "fake-plugin");
verify(systemVersionSupplier).get();
verify(pluginProperties).getPluginsRoot();
verify(client).create(isA(Plugin.class));
}
@ -181,6 +178,8 @@ class PluginServiceImplTest {
@Test
void upgradeNormally() {
when(pluginsRootGetter.get()).thenReturn(tempDirectory.resolve("plugins"));
var oldFakePlugin = createPlugin("fake-plugin", plugin -> {
plugin.getSpec().setEnabled(true);
plugin.getSpec().setVersion("0.0.1");

View File

@ -22,7 +22,7 @@ class DefaultPluginApplicationContextFactoryTest {
@BeforeEach
void setUp() {
factory = new DefaultPluginApplicationContextFactory((SpringPluginManager) pluginManager);
factory = new DefaultPluginApplicationContextFactory(pluginManager);
}
@Test
@ -41,6 +41,7 @@ class DefaultPluginApplicationContextFactoryTest {
assertInstanceOf(PluginApplicationContext.class, context);
assertNotNull(context.getBeanProvider(SearchService.class).getIfUnique());
assertNotNull(context.getBeanProvider(PluginsRootGetter.class).getIfUnique());
// TODO Add more assertions here.
}

View File

@ -0,0 +1,30 @@
package run.halo.app.plugin;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;
import java.nio.file.Paths;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import run.halo.app.infra.properties.HaloProperties;
@ExtendWith(MockitoExtension.class)
class PluginsRootGetterImplTest {
@Mock
HaloProperties haloProperties;
@InjectMocks
PluginsRootGetterImpl pluginsRootGetter;
@Test
void shouldGetterPluginsRootCorrectly() {
var haloWorkDir = Paths.get("halo-work-dir");
when(haloProperties.getWorkDir()).thenReturn(haloWorkDir);
assertEquals(haloWorkDir.resolve("plugins"), pluginsRootGetter.get());
}
}