Allow configuration of external access url (#2385)

#### What type of PR is this?

/kind feature
/area core
/milestone 2.0

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

Many permanent links will be generated in the system, in order to ensure accessiblity, we will allow users to configure external access links instead of internal url.

#### Special notes for your reviewer:

Before starting the application, we need to modify the following configuration into the configuration file(application.yaml):

```yaml
halo:
  external-url: https://halo.run
```

Then, after booting the application, check for permalinks of attachments.

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

```release-note
支持在配置文件中修改博客访问链接
```
pull/2382/head^2
John Niang 2022-09-07 11:54:11 +08:00 committed by GitHub
parent 5118434db2
commit de40a108e2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 126 additions and 38 deletions

View File

@ -46,6 +46,7 @@ import run.halo.app.extension.controller.Controller;
import run.halo.app.extension.controller.ControllerBuilder;
import run.halo.app.extension.controller.ControllerManager;
import run.halo.app.extension.router.ExtensionCompositeRouterFunction;
import run.halo.app.infra.ExternalUrlSupplier;
import run.halo.app.infra.properties.HaloProperties;
import run.halo.app.plugin.HaloPluginManager;
import run.halo.app.plugin.resources.JsBundleRuleProvider;
@ -173,9 +174,10 @@ public class ExtensionConfiguration {
}
@Bean
Controller attachmentController(ExtensionClient client, PluginManager pluginManager) {
Controller attachmentController(ExtensionClient client, PluginManager pluginManager,
ExternalUrlSupplier externalUrl) {
return new ControllerBuilder("attachment-controller", client)
.reconciler(new AttachmentReconciler(client, pluginManager))
.reconciler(new AttachmentReconciler(client, pluginManager, externalUrl))
.extension(new Attachment())
.build();
}

View File

@ -5,14 +5,12 @@ import static org.springdoc.core.fn.builders.content.Builder.contentBuilder;
import static org.springdoc.core.fn.builders.schema.Builder.schemaBuilder;
import static org.springframework.web.reactive.function.BodyExtractors.toMultipartData;
import static org.springframework.web.reactive.function.server.RequestPredicates.contentType;
import static run.halo.app.core.extension.attachment.Constant.FINALIZER_NAME;
import static run.halo.app.extension.ListResult.generateGenericClass;
import static run.halo.app.extension.router.QueryParamBuildUtil.buildParametersFromType;
import static run.halo.app.extension.router.selector.SelectorUtil.labelAndFieldSelectorToPredicate;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import lombok.extern.slf4j.Slf4j;
import org.pf4j.PluginManager;
@ -125,8 +123,6 @@ public class AttachmentEndpoint implements CustomEndpoint {
// validate the group name
spec.setGroupRef(Ref.of(groupName));
}
// set finalizers mandatory
attachment.getMetadata().setFinalizers(Set.of(FINALIZER_NAME));
}))
.next()
.switchIfEmpty(Mono.error(

View File

@ -1,9 +1,9 @@
package run.halo.app.core.extension.reconciler.attachment;
import java.nio.charset.StandardCharsets;
import java.util.HashSet;
import java.util.Set;
import lombok.extern.slf4j.Slf4j;
import org.pf4j.PluginManager;
import org.springframework.web.util.UriUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import run.halo.app.core.extension.attachment.Attachment;
@ -16,6 +16,7 @@ import run.halo.app.extension.ConfigMap;
import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.controller.Reconciler;
import run.halo.app.extension.controller.Reconciler.Request;
import run.halo.app.infra.ExternalUrlSupplier;
import run.halo.app.infra.exception.NotFoundException;
@Slf4j
@ -25,9 +26,13 @@ public class AttachmentReconciler implements Reconciler<Request> {
private final PluginManager pluginManager;
public AttachmentReconciler(ExtensionClient client, PluginManager pluginManager) {
private final ExternalUrlSupplier externalUrl;
public AttachmentReconciler(ExtensionClient client, PluginManager pluginManager,
ExternalUrlSupplier externalUrl) {
this.client = client;
this.pluginManager = pluginManager;
this.externalUrl = externalUrl;
}
@Override
@ -43,22 +48,25 @@ public class AttachmentReconciler implements Reconciler<Request> {
.orElseThrow();
var deleteOption = new DeleteOption(attachment, policy, configMap);
Flux.fromIterable(pluginManager.getExtensions(AttachmentHandler.class))
.concatMap(handler -> handler.delete(deleteOption))
.next()
.switchIfEmpty(Mono.error(() -> new NotFoundException(
"No suitable handler found to delete the attachment")))
.doOnNext(deleted -> removeFinalizer(deleted.getMetadata().getName()))
.block();
.concatMap(handler -> handler.delete(deleteOption)).next().switchIfEmpty(
Mono.error(() -> new NotFoundException(
"No suitable handler found to delete the attachment")))
.doOnNext(deleted -> removeFinalizer(deleted.getMetadata().getName())).block();
return;
}
// add finalizer
addFinalizerIfNotSet(request.name(), attachment.getMetadata().getFinalizers());
var annotations = attachment.getMetadata().getAnnotations();
if (annotations != null) {
String permalink = null;
var localRelativePath = annotations.get(Constant.LOCAL_REL_PATH_ANNO_KEY);
if (localRelativePath != null) {
// TODO Add router function here.
permalink = "http://localhost:8090/upload/" + localRelativePath;
permalink = UriUtils.encodePath(permalink, StandardCharsets.UTF_8);
permalink = externalUrl.get()
.resolve("/upload/" + localRelativePath)
.normalize()
.toASCIIString();
} else {
var externalLink = annotations.get(Constant.EXTERNAL_LINK_ANNO_KEY);
if (externalLink != null) {
@ -85,14 +93,29 @@ public class AttachmentReconciler implements Reconciler<Request> {
}
void removeFinalizer(String attachmentName) {
client.fetch(Attachment.class, attachmentName)
.ifPresent(attachment -> {
var finalizers = attachment.getMetadata().getFinalizers();
if (finalizers != null && finalizers.remove(Constant.FINALIZER_NAME)) {
// update it
client.update(attachment);
}
});
client.fetch(Attachment.class, attachmentName).ifPresent(attachment -> {
var finalizers = attachment.getMetadata().getFinalizers();
if (finalizers != null && finalizers.remove(Constant.FINALIZER_NAME)) {
// update it
client.update(attachment);
}
});
}
void addFinalizerIfNotSet(String attachmentName, Set<String> existingFinalizers) {
if (existingFinalizers != null && existingFinalizers.contains(Constant.FINALIZER_NAME)) {
return;
}
client.fetch(Attachment.class, attachmentName).ifPresent(attachment -> {
var finalizers = attachment.getMetadata().getFinalizers();
if (finalizers == null) {
finalizers = new HashSet<>();
attachment.getMetadata().setFinalizers(finalizers);
}
finalizers.add(Constant.FINALIZER_NAME);
client.update(attachment);
});
}
}

View File

@ -0,0 +1,13 @@
package run.halo.app.infra;
import java.net.URI;
import java.util.function.Supplier;
/**
* Represents a supplier of external url configuration.
*
* @author johnniang
*/
public interface ExternalUrlSupplier extends Supplier<URI> {
}

View File

@ -0,0 +1,25 @@
package run.halo.app.infra;
import java.net.URI;
import org.springframework.stereotype.Component;
import run.halo.app.infra.properties.HaloProperties;
/**
* Default implementation for getting external url from halo properties.
*
* @author johnniang
*/
@Component
public class HaloPropertiesExternalUrlSupplier implements ExternalUrlSupplier {
private final HaloProperties haloProperties;
public HaloPropertiesExternalUrlSupplier(HaloProperties haloProperties) {
this.haloProperties = haloProperties;
}
@Override
public URI get() {
return haloProperties.getExternalUrl();
}
}

View File

@ -1,10 +1,14 @@
package run.halo.app.infra.properties;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import java.net.URI;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.Set;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
/**
* @author guqing
@ -12,10 +16,15 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
*/
@Data
@ConfigurationProperties(prefix = "halo")
@Validated
public class HaloProperties {
@NotNull
private Path workDir;
@NotNull
private URI externalUrl;
private Set<String> initialExtensionLocations = new HashSet<>();
/**
@ -25,7 +34,9 @@ public class HaloProperties {
*/
private boolean requiredExtensionDisabled;
@Valid
private final ExtensionProperties extension = new ExtensionProperties();
@Valid
private final SecurityProperties security = new SecurityProperties();
}

View File

@ -14,6 +14,7 @@ spring:
platform: h2
halo:
external-url: "http://${server.address:localhost}:${server.port}"
security:
oauth2:
jwt:

View File

@ -2,13 +2,10 @@ package run.halo.app;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.method.HandlerTypePredicate;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* Test case for api path prefix predicate.
@ -37,14 +34,4 @@ public class PathPrefixPredicateTest {
}
@Test
void test() {
Flux.fromIterable(List.of(1, 2, 3))
.flatMap(i -> {
if (i == 2) {
return Mono.empty();
}
return Mono.just(i);
}).subscribe(System.out::println);
}
}

View File

@ -0,0 +1,29 @@
package run.halo.app.infra;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.net.URI;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import run.halo.app.infra.properties.HaloProperties;
@ExtendWith(MockitoExtension.class)
class HaloPropertiesExternalUrlSupplierTest {
@Mock
HaloProperties haloProperties;
@InjectMocks
HaloPropertiesExternalUrlSupplier externalUrl;
@Test
void get() {
URI fakeUri = URI.create("fake-url");
Mockito.when(haloProperties.getExternalUrl()).thenReturn(fakeUri);
assertEquals(fakeUri, externalUrl.get());
}
}

View File

@ -14,6 +14,7 @@ spring:
halo:
work-dir: ${user.home}/halo-next-test
external-url: "http://${server.address:localhost}:${server.port}"
security:
initializer:
disabled: true