diff --git a/src/main/java/run/halo/app/infra/ExtensionResourceInitializer.java b/src/main/java/run/halo/app/infra/ExtensionResourceInitializer.java index ee0657e0c..292d67d3d 100644 --- a/src/main/java/run/halo/app/infra/ExtensionResourceInitializer.java +++ b/src/main/java/run/halo/app/infra/ExtensionResourceInitializer.java @@ -1,21 +1,17 @@ package run.halo.app.infra; import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; +import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.stream.Stream; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.ApplicationListener; -import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.lang.NonNull; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; -import org.thymeleaf.util.StringUtils; import run.halo.app.extension.ExtensionClient; import run.halo.app.extension.Unstructured; import run.halo.app.infra.properties.HaloProperties; @@ -32,6 +28,9 @@ import run.halo.app.infra.utils.YamlUnstructuredLoader; @Slf4j @Component public class ExtensionResourceInitializer implements ApplicationListener { + + public static final Set REQUIRED_EXTENSION_LOCATIONS = + Set.of("classpath:/extensions/*.yaml", "classpath:/extensions/*.yml"); private final HaloProperties haloProperties; private final ExtensionClient extensionClient; @@ -43,40 +42,46 @@ public class ExtensionResourceInitializer implements ApplicationListener extensionLocations = haloProperties.getInitialExtensionLocations(); - if (!CollectionUtils.isEmpty(extensionLocations)) { + var locations = new HashSet(); + if (!haloProperties.isRequiredExtensionDisabled()) { + locations.addAll(REQUIRED_EXTENSION_LOCATIONS); + } + if (haloProperties.getInitialExtensionLocations() != null) { + locations.addAll(haloProperties.getInitialExtensionLocations()); + } - Resource[] resources = extensionLocations.stream() - .map(this::listYamlFiles) - .flatMap(List::stream) - .toArray(Resource[]::new); + if (CollectionUtils.isEmpty(locations)) { + return; + } - log.debug("Initialization loaded [{}] resources to establish.", resources.length); + var resources = locations.stream() + .map(this::listResources) + .flatMap(List::stream) + .distinct() + .toArray(Resource[]::new); - new YamlUnstructuredLoader(resources).load() - .forEach(unstructured -> extensionClient.fetch(unstructured.groupVersionKind(), - unstructured.getMetadata().getName()) - .ifPresentOrElse(persisted -> { - unstructured.getMetadata() - .setVersion(persisted.getMetadata().getVersion()); - extensionClient.update(unstructured); - }, () -> extensionClient.create(unstructured))); + log.info("Initializing [{}] extensions in locations: {}", resources.length, locations); + + new YamlUnstructuredLoader(resources).load() + .forEach(unstructured -> extensionClient.fetch(unstructured.groupVersionKind(), + unstructured.getMetadata().getName()) + .ifPresentOrElse(persisted -> { + unstructured.getMetadata() + .setVersion(persisted.getMetadata().getVersion()); + // TODO Patch the unstructured instead of update it in the future + extensionClient.update(unstructured); + }, () -> extensionClient.create(unstructured))); + + log.info("Initialized [{}] extensions in locations: {}", resources.length, locations); + } + + private List listResources(String location) { + var resolver = new PathMatchingResourcePatternResolver(); + try { + return List.of(resolver.getResources(location)); + } catch (IOException ie) { + throw new IllegalArgumentException("Invalid extension location: " + location, ie); } } - private List listYamlFiles(String location) { - try (Stream walk = Files.walk(Paths.get(location))) { - return walk.filter(this::isYamlFile) - .map(path -> new FileSystemResource(path.toFile())) - .toList(); - } catch (IOException e) { - throw new IllegalArgumentException(e); - } - } - - private boolean isYamlFile(Path pathname) { - Path fileName = pathname.getFileName(); - return StringUtils.endsWith(fileName, ".yaml") - || StringUtils.endsWith(fileName, ".yml"); - } } diff --git a/src/main/java/run/halo/app/infra/properties/HaloProperties.java b/src/main/java/run/halo/app/infra/properties/HaloProperties.java index 59ebec731..f0bdc75a6 100644 --- a/src/main/java/run/halo/app/infra/properties/HaloProperties.java +++ b/src/main/java/run/halo/app/infra/properties/HaloProperties.java @@ -18,6 +18,13 @@ public class HaloProperties { private Set initialExtensionLocations = new HashSet<>(); + /** + * This property could stop initializing required Extensions defined in classpath. + * See {@link run.halo.app.infra.ExtensionResourceInitializer#REQUIRED_EXTENSION_LOCATIONS} + * for more. + */ + private boolean requiredExtensionDisabled; + private final ExtensionProperties extension = new ExtensionProperties(); private final SecurityProperties security = new SecurityProperties(); diff --git a/src/test/java/run/halo/app/infra/ExtensionResourceInitializerTest.java b/src/test/java/run/halo/app/infra/ExtensionResourceInitializerTest.java index 4e7316e98..9038fd013 100644 --- a/src/test/java/run/halo/app/infra/ExtensionResourceInitializerTest.java +++ b/src/test/java/run/halo/app/infra/ExtensionResourceInitializerTest.java @@ -10,10 +10,12 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.Set; import org.json.JSONException; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -22,6 +24,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.skyscreamer.jsonassert.JSONAssert; import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.util.FileSystemUtils; import run.halo.app.extension.ExtensionClient; import run.halo.app.extension.GroupVersionKind; import run.halo.app.extension.Unstructured; @@ -46,13 +49,19 @@ class ExtensionResourceInitializerTest { private ExtensionResourceInitializer extensionResourceInitializer; + List dirsToClean; + @BeforeEach void setUp() throws IOException { extensionResourceInitializer = new ExtensionResourceInitializer(haloProperties, extensionClient); + dirsToClean = new ArrayList<>(2); + Path tempDirectory = Files.createTempDirectory("extension-resource-initializer-test"); - Path multiDirectory = Files.createDirectories(tempDirectory.resolve("a/b/c")); + dirsToClean.add(tempDirectory); + Path multiDirectory = + Files.createDirectories(tempDirectory.resolve("a").resolve("b").resolve("c")); Files.writeString(tempDirectory.resolve("hello.yml"), """ kind: FakeExtension apiVersion: v1 @@ -79,8 +88,9 @@ class ExtensionResourceInitializerTest { StandardCharsets.UTF_8); // test file in directory - Path filePath = Files.createTempDirectory("extension-resource-file-test") - .resolve("good.yml"); + Path secondTempDir = Files.createTempDirectory("extension-resource-file-test"); + dirsToClean.add(secondTempDir); + Path filePath = secondTempDir.resolve("good.yml"); Files.writeString(filePath, """ kind: FakeExtension apiVersion: v1 @@ -92,11 +102,23 @@ class ExtensionResourceInitializerTest { StandardCharsets.UTF_8); when(haloProperties.getInitialExtensionLocations()) - .thenReturn(Set.of(tempDirectory.toString(), filePath.toString())); + .thenReturn(Set.of("file:" + tempDirectory + "/**/*.yaml", + "file:" + tempDirectory + "/**/*.yml", + "file:" + filePath)); + } + + @AfterEach + void cleanUp() throws IOException { + if (dirsToClean != null) { + for (var dir : dirsToClean) { + FileSystemUtils.deleteRecursively(dir); + } + } } @Test void onApplicationEvent() throws JSONException { + when(haloProperties.isRequiredExtensionDisabled()).thenReturn(true); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Unstructured.class); when(extensionClient.fetch(any(GroupVersionKind.class), any()))