mirror of https://github.com/halo-dev/halo
Refactor thumbnail handling to streamline thumbnail URI generation and enhance image processing
parent
ce2cfba4c6
commit
7c1c25348f
|
@ -1,5 +1,6 @@
|
|||
package run.halo.app.core.attachment;
|
||||
|
||||
import java.util.Optional;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
|
@ -40,4 +41,13 @@ public enum ThumbnailSize {
|
|||
}
|
||||
throw new IllegalArgumentException("No such thumbnail size: " + name);
|
||||
}
|
||||
|
||||
public static Optional<ThumbnailSize> optionalValueOf(String name) {
|
||||
for (ThumbnailSize value : values()) {
|
||||
if (value.name().equalsIgnoreCase(name)) {
|
||||
return Optional.of(value);
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,92 +0,0 @@
|
|||
package run.halo.app.content;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.function.Function;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.select.Elements;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.attachment.ThumbnailService;
|
||||
import run.halo.app.core.attachment.ThumbnailSize;
|
||||
|
||||
@UtilityClass
|
||||
public class HtmlThumbnailSrcsetInjector {
|
||||
static final String SRC = "src";
|
||||
static final String SRCSET = "srcset";
|
||||
|
||||
/**
|
||||
* Inject srcset attribute to img tags in the given html.
|
||||
*/
|
||||
public static Mono<String> injectSrcset(String html,
|
||||
Function<String, Mono<String>> srcSetValueGenerator) {
|
||||
Document document = Jsoup.parseBodyFragment(html);
|
||||
document.outputSettings(new Document.OutputSettings().prettyPrint(false));
|
||||
|
||||
Elements imgTags = document.select("img[src]");
|
||||
return Flux.fromIterable(imgTags)
|
||||
.filter(element -> {
|
||||
String src = element.attr(SRC);
|
||||
return !element.hasAttr(SRCSET) && isValidSrc(src);
|
||||
})
|
||||
.flatMap(img -> {
|
||||
String src = img.attr(SRC);
|
||||
return srcSetValueGenerator.apply(src)
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.doOnNext(srcsetValue -> {
|
||||
img.attr(SRCSET, srcsetValue);
|
||||
img.attr("sizes", buildSizesAttr());
|
||||
});
|
||||
})
|
||||
.then(Mono.fromSupplier(() -> document.body().html()));
|
||||
}
|
||||
|
||||
static String buildSizesAttr() {
|
||||
var sb = new StringBuilder();
|
||||
var delimiter = ", ";
|
||||
var sizes = ThumbnailSize.values();
|
||||
for (int i = 0; i < sizes.length; i++) {
|
||||
var size = sizes[i];
|
||||
sb.append("(max-width: ").append(size.getWidth()).append("px)")
|
||||
.append(" ")
|
||||
.append(size.getWidth())
|
||||
.append("px");
|
||||
if (i < sizes.length - 1) {
|
||||
sb.append(delimiter);
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate srcset attribute value for the given src.
|
||||
*/
|
||||
public static Mono<String> generateSrcset(URI src, ThumbnailService thumbnailService) {
|
||||
return Flux.fromArray(ThumbnailSize.values())
|
||||
.flatMap(size -> thumbnailService.get(src, size)
|
||||
.map(thumbnail -> thumbnail.toString() + " " + size.getWidth() + "w")
|
||||
)
|
||||
.collect(StringBuilder::new, (builder, srcsetValue) -> {
|
||||
if (!builder.isEmpty()) {
|
||||
builder.append(", ");
|
||||
}
|
||||
builder.append(srcsetValue);
|
||||
})
|
||||
.map(StringBuilder::toString);
|
||||
}
|
||||
|
||||
private static boolean isValidSrc(String src) {
|
||||
if (StringUtils.isBlank(src)) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
URI.create(src);
|
||||
return true;
|
||||
} catch (IllegalArgumentException e) {
|
||||
// ignore
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
package run.halo.app.content;
|
||||
|
||||
import static run.halo.app.content.HtmlThumbnailSrcsetInjector.generateSrcset;
|
||||
|
||||
import java.net.URI;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.stereotype.Component;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.attachment.ThumbnailService;
|
||||
import run.halo.app.theme.ReactiveSinglePageContentHandler;
|
||||
|
||||
/**
|
||||
* A single page content handler to handle post html content and generate thumbnail by the img tag.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.21.0
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class PageContentThumbnailHandler implements ReactiveSinglePageContentHandler {
|
||||
private final ThumbnailService thumbnailService;
|
||||
|
||||
@Override
|
||||
public Mono<SinglePageContentContext> handle(
|
||||
@NonNull SinglePageContentContext singlePageContent) {
|
||||
var html = singlePageContent.getContent();
|
||||
return HtmlThumbnailSrcsetInjector.injectSrcset(html,
|
||||
src -> generateSrcset(URI.create(src), thumbnailService)
|
||||
)
|
||||
.onErrorResume(throwable -> {
|
||||
log.debug("Failed to inject srcset to page content, fallback to original content",
|
||||
throwable);
|
||||
return Mono.just(html);
|
||||
})
|
||||
.doOnNext(singlePageContent::setContent)
|
||||
.thenReturn(singlePageContent);
|
||||
}
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
package run.halo.app.content;
|
||||
|
||||
import static run.halo.app.content.HtmlThumbnailSrcsetInjector.generateSrcset;
|
||||
|
||||
import java.net.URI;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.stereotype.Component;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.attachment.ThumbnailService;
|
||||
import run.halo.app.theme.ReactivePostContentHandler;
|
||||
|
||||
/**
|
||||
* A post content handler to handle post html content and generate thumbnail by the img tag.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.19.0
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class PostContentThumbnailHandler implements ReactivePostContentHandler {
|
||||
private final ThumbnailService thumbnailService;
|
||||
|
||||
@Override
|
||||
public Mono<PostContentContext> handle(@NonNull PostContentContext postContent) {
|
||||
var html = postContent.getContent();
|
||||
return HtmlThumbnailSrcsetInjector.injectSrcset(html,
|
||||
src -> generateSrcset(URI.create(src), thumbnailService)
|
||||
)
|
||||
.onErrorResume(throwable -> {
|
||||
log.debug("Failed to inject srcset to post content, fallback to original content",
|
||||
throwable);
|
||||
return Mono.just(html);
|
||||
})
|
||||
.doOnNext(postContent::setContent)
|
||||
.thenReturn(postContent);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
package run.halo.app.core.attachment;
|
||||
|
||||
import static org.thymeleaf.templatemode.TemplateMode.HTML;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.thymeleaf.context.ITemplateContext;
|
||||
import org.thymeleaf.engine.ElementNames;
|
||||
import org.thymeleaf.model.IAttribute;
|
||||
import org.thymeleaf.model.IProcessableElementTag;
|
||||
import org.thymeleaf.processor.element.MatchingElementName;
|
||||
import org.thymeleaf.spring6.context.SpringContextUtils;
|
||||
import org.thymeleaf.spring6.context.webflux.SpringWebFluxThymeleafRequestContext;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.infra.ExternalUrlSupplier;
|
||||
import run.halo.app.theme.dialect.ElementTagPostProcessor;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
class ThumbnailImgTagPostProcessor implements ElementTagPostProcessor {
|
||||
|
||||
private final MatchingElementName matchingElementName;
|
||||
|
||||
private final ExternalUrlSupplier externalUrlSupplier;
|
||||
|
||||
public ThumbnailImgTagPostProcessor(ExternalUrlSupplier externalUrlSupplier) {
|
||||
this.externalUrlSupplier = externalUrlSupplier;
|
||||
this.matchingElementName =
|
||||
MatchingElementName.forElementName(HTML, ElementNames.forHTMLName("img"));
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Mono<IProcessableElementTag> process(ITemplateContext context,
|
||||
IProcessableElementTag tag) {
|
||||
if (!matchingElementName.matches(tag.getElementDefinition().getElementName())) {
|
||||
return Mono.empty();
|
||||
}
|
||||
if (tag.hasAttribute("srcset")) {
|
||||
return Mono.empty();
|
||||
}
|
||||
var srcValue = Optional.ofNullable(tag.getAttribute("src"))
|
||||
.map(IAttribute::getValue)
|
||||
.filter(StringUtils::hasText)
|
||||
.map(URI::create);
|
||||
if (srcValue.isEmpty()) {
|
||||
log.debug("Skip processing img tag without src attribute");
|
||||
return Mono.empty();
|
||||
}
|
||||
// get img tag
|
||||
var imageUri = srcValue.get();
|
||||
|
||||
if (imageUri.isAbsolute()) {
|
||||
// check if the uri is belonged to current site
|
||||
var requestContext = SpringContextUtils.getRequestContext(context);
|
||||
if (!(requestContext instanceof SpringWebFluxThymeleafRequestContext wrc)) {
|
||||
log.debug("Skip processing img tag with absolute url: {}, "
|
||||
+ "because the request context is not webflux", imageUri);
|
||||
return Mono.empty();
|
||||
}
|
||||
var externalUri = externalUrlSupplier.get();
|
||||
if (!externalUri.isAbsolute()) {
|
||||
externalUri = wrc.getServerWebExchange().getRequest().getURI();
|
||||
}
|
||||
if (!Objects.equals(externalUri.getAuthority(), imageUri.getAuthority())) {
|
||||
log.debug("""
|
||||
Skip processing img tag with external absolute url: {} because \
|
||||
the url does not belong to the current site\
|
||||
""", imageUri);
|
||||
return Mono.empty();
|
||||
}
|
||||
}
|
||||
|
||||
var fileSuffix = FilenameUtils.getExtension(imageUri.getPath());
|
||||
if (!ThumbnailUtils.isSupportedImage(fileSuffix)) {
|
||||
log.debug("Skip processing img tag with unsupported image suffix: {}", fileSuffix);
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
// build thumbnails
|
||||
var thumbnails = ThumbnailUtils.buildSrcsetMap(imageUri);
|
||||
if (CollectionUtils.isEmpty(thumbnails)) {
|
||||
log.debug("Skip processing img tag because the image is not supported: {}", imageUri);
|
||||
return Mono.empty();
|
||||
}
|
||||
var modelFactory = context.getModelFactory();
|
||||
tag = modelFactory.setAttribute(tag, "size", """
|
||||
(max-width: 400px) 400px, \
|
||||
(max-width: 800px) 800px, \
|
||||
(max-width: 1200px) 1200px, \
|
||||
(max-width: 1600px) 1600px\
|
||||
""");
|
||||
|
||||
var srcset = thumbnails.keySet().stream()
|
||||
.map(size -> {
|
||||
var uri = thumbnails.get(size);
|
||||
return uri + " " + size.getWidth() + "w";
|
||||
})
|
||||
.collect(Collectors.joining(", "));
|
||||
tag = modelFactory.setAttribute(tag, "srcset", srcset);
|
||||
return Mono.just(tag);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,9 +1,14 @@
|
|||
package run.halo.app.core.attachment;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.util.MimeType;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
public enum ThumbnailUtils {
|
||||
;
|
||||
|
@ -33,4 +38,25 @@ public enum ThumbnailUtils {
|
|||
return SUPPORTED_IMAGE_MIME_TYPES.stream()
|
||||
.anyMatch(supported -> supported.isCompatibleWith(mimeType));
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a map of thumbnail size to its corresponding URI based on the given permalink.
|
||||
*
|
||||
* @param permalink permalink of the attachment in local storage
|
||||
* @return a map where the key is the thumbnail size and the value is the URI of the thumbnail
|
||||
*/
|
||||
public static Map<ThumbnailSize, URI> buildSrcsetMap(URI permalink) {
|
||||
var fileSuffix = FilenameUtils.getExtension(permalink.getPath());
|
||||
if (!isSupportedImage(fileSuffix)) {
|
||||
return Map.of();
|
||||
}
|
||||
return Arrays.stream(ThumbnailSize.values())
|
||||
.collect(Collectors.toMap(t -> t, t -> {
|
||||
var prefix = "/thumbnails/w" + t.getWidth();
|
||||
return UriComponentsBuilder.fromUri(permalink)
|
||||
.replacePath(prefix + permalink.getPath())
|
||||
.build()
|
||||
.toUri();
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,6 @@ import java.util.Optional;
|
|||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.Data;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.RandomStringUtils;
|
||||
|
@ -311,20 +310,8 @@ class LocalAttachmentUploadHandler implements AttachmentHandler {
|
|||
|| !StringUtils.hasText(attachment.getStatus().getPermalink())) {
|
||||
return Mono.just(Map.of());
|
||||
}
|
||||
var mediaType = MediaType.parseMediaType(attachment.getSpec().getMediaType());
|
||||
if (!ThumbnailUtils.isSupportedImage(mediaType)) {
|
||||
return Mono.just(Map.of());
|
||||
}
|
||||
|
||||
var thumbnails = Arrays.stream(ThumbnailSize.values())
|
||||
.collect(Collectors.toMap(t -> t, t -> {
|
||||
var permalink = URI.create(attachment.getStatus().getPermalink());
|
||||
var prefix = "/thumbnails/w" + t.getWidth();
|
||||
return UriComponentsBuilder.fromUri(permalink)
|
||||
.replacePath(prefix + permalink.getPath())
|
||||
.build()
|
||||
.toUri();
|
||||
}));
|
||||
var permalinkUri = URI.create(attachment.getStatus().getPermalink());
|
||||
var thumbnails = ThumbnailUtils.buildSrcsetMap(permalinkUri);
|
||||
return Mono.just(thumbnails);
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,6 @@ import lombok.RequiredArgsConstructor;
|
|||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.stereotype.Component;
|
||||
import run.halo.app.core.attachment.AttachmentRootGetter;
|
||||
import run.halo.app.core.attachment.AttachmentUtils;
|
||||
import run.halo.app.core.attachment.LocalThumbnailService;
|
||||
|
@ -36,7 +35,7 @@ import run.halo.app.extension.controller.Reconciler;
|
|||
import run.halo.app.infra.ExternalLinkProcessor;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
// @Component
|
||||
@RequiredArgsConstructor
|
||||
public class LocalThumbnailsReconciler implements Reconciler<Reconciler.Request> {
|
||||
private final LocalThumbnailService localThumbnailService;
|
||||
|
|
|
@ -6,34 +6,28 @@ import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;
|
|||
|
||||
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springdoc.core.fn.builders.operation.Builder;
|
||||
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder;
|
||||
import org.springframework.boot.autoconfigure.web.WebProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.CacheControl;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.reactive.function.BodyInserters;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.RouterFunctions;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import org.springframework.web.server.ServerWebInputException;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.attachment.LocalThumbnailService;
|
||||
import run.halo.app.core.attachment.ThumbnailService;
|
||||
import run.halo.app.core.attachment.ThumbnailSize;
|
||||
import run.halo.app.core.extension.attachment.Attachment;
|
||||
import run.halo.app.core.extension.endpoint.CustomEndpoint;
|
||||
import run.halo.app.extension.ExtensionUtil;
|
||||
import run.halo.app.extension.GroupVersion;
|
||||
import run.halo.app.extension.ListOptions;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.extension.index.query.QueryFactory;
|
||||
|
||||
/**
|
||||
* Thumbnail endpoint for thumbnail resource access.
|
||||
|
@ -44,10 +38,8 @@ import run.halo.app.extension.GroupVersion;
|
|||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class ThumbnailEndpoint implements CustomEndpoint {
|
||||
private final WebClient webClient = WebClient.builder().build();
|
||||
private final LocalThumbnailService localThumbnailService;
|
||||
private final WebProperties webProperties;
|
||||
private final ThumbnailService thumbnailService;
|
||||
|
||||
private final ReactiveExtensionClient client;
|
||||
|
||||
@Override
|
||||
public RouterFunction<ServerResponse> endpoint() {
|
||||
|
@ -66,27 +58,25 @@ public class ThumbnailEndpoint implements CustomEndpoint {
|
|||
|
||||
private Mono<ServerResponse> getThumbnailByUri(ServerRequest request) {
|
||||
var query = new ThumbnailQuery(request.queryParams());
|
||||
return thumbnailService.get(query.getUri(), query.getSize())
|
||||
.filterWhen(uri -> isAccessible(request, uri))
|
||||
.defaultIfEmpty(query.getUri())
|
||||
.flatMap(uri -> ServerResponse.temporaryRedirect(uri).build());
|
||||
}
|
||||
|
||||
Mono<Boolean> isAccessible(ServerRequest request, URI uri) {
|
||||
var url = Optional.of(uri)
|
||||
.filter(URI::isAbsolute)
|
||||
.orElseGet(() -> request.uriBuilder().replacePath(uri.toASCIIString()).build());
|
||||
// resource handler does not support head access for Halo, so use get request here
|
||||
return webClient.get()
|
||||
.uri(url)
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range
|
||||
.header(HttpHeaders.RANGE, "bytes=0-0")
|
||||
.exchangeToMono(response -> {
|
||||
var statusCode = response.statusCode();
|
||||
return Mono.just(statusCode.is2xxSuccessful() || statusCode.is3xxRedirection());
|
||||
var size = query.getSize();
|
||||
var uri = query.getUri().toASCIIString();
|
||||
var listOptions = ListOptions.builder()
|
||||
.andQuery(ExtensionUtil.notDeleting())
|
||||
.andQuery(QueryFactory.equal("status.permalink", uri))
|
||||
.build();
|
||||
// query by permalink
|
||||
return client.listAll(Attachment.class, listOptions, ExtensionUtil.defaultSort())
|
||||
// find the first one
|
||||
.next()
|
||||
.mapNotNull(attachment -> {
|
||||
var thumbnails = attachment.getStatus().getThumbnails();
|
||||
return thumbnails.get(size.name());
|
||||
})
|
||||
.onErrorReturn(false)
|
||||
.defaultIfEmpty(false);
|
||||
.defaultIfEmpty(uri)
|
||||
.flatMap(thumbnailLink -> ServerResponse.status(HttpStatus.FOUND)
|
||||
.location(URI.create(thumbnailLink))
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
static class ThumbnailQuery {
|
||||
|
@ -132,61 +122,9 @@ public class ThumbnailEndpoint implements CustomEndpoint {
|
|||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
RouterFunction<ServerResponse> localThumbnailResourceRouter() {
|
||||
return RouterFunctions.route()
|
||||
.GET("/upload/thumbnails/{year}/w{width}/{fileName}", request -> {
|
||||
var width = request.pathVariable("width");
|
||||
var year = request.pathVariable("year");
|
||||
var fileName = request.pathVariable("fileName");
|
||||
var size = ThumbnailSize.fromWidth(width);
|
||||
var thumbnailUri = localThumbnailService.buildThumbnailUri(year, size, fileName);
|
||||
return localThumbnailService.getThumbnail(thumbnailUri)
|
||||
.flatMap(resource -> getResourceResponse(request, resource))
|
||||
.switchIfEmpty(Mono.defer(
|
||||
() -> localThumbnailService.getOriginalImageUri(thumbnailUri)
|
||||
.flatMap(this::fallback))
|
||||
);
|
||||
})
|
||||
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public GroupVersion groupVersion() {
|
||||
return new GroupVersion("api.storage.halo.run", "v1alpha1");
|
||||
}
|
||||
|
||||
private Mono<ServerResponse> getResourceResponse(ServerRequest request, Resource resource) {
|
||||
var resourceProperties = webProperties.getResources();
|
||||
final var useLastModified = resourceProperties.getCache().isUseLastModified();
|
||||
final var cacheControl = getCacheControl(resourceProperties);
|
||||
var bodyBuilder = ServerResponse.ok().cacheControl(cacheControl);
|
||||
try {
|
||||
if (useLastModified) {
|
||||
var lastModified = Instant.ofEpochMilli(resource.lastModified());
|
||||
return request.checkNotModified(lastModified)
|
||||
.switchIfEmpty(Mono.defer(() -> bodyBuilder.lastModified(lastModified)
|
||||
.body(BodyInserters.fromResource(resource))
|
||||
));
|
||||
}
|
||||
return bodyBuilder.body(BodyInserters.fromResource(resource));
|
||||
} catch (IOException e) {
|
||||
return Mono.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
private Mono<ServerResponse> fallback(URI imageUri) {
|
||||
return ServerResponse.temporaryRedirect(imageUri).build();
|
||||
}
|
||||
|
||||
private static CacheControl getCacheControl(WebProperties.Resources resourceProperties) {
|
||||
var cacheControl = resourceProperties.getCache()
|
||||
.getCachecontrol()
|
||||
.toHttpCacheControl();
|
||||
if (cacheControl == null) {
|
||||
cacheControl = CacheControl.empty();
|
||||
}
|
||||
return cacheControl;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
package run.halo.app.theme.finders.impl;
|
||||
|
||||
import java.net.URI;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.attachment.ThumbnailService;
|
||||
import run.halo.app.core.attachment.ThumbnailSize;
|
||||
import run.halo.app.theme.finders.Finder;
|
||||
import run.halo.app.theme.finders.ThumbnailFinder;
|
||||
|
||||
|
@ -13,18 +10,10 @@ import run.halo.app.theme.finders.ThumbnailFinder;
|
|||
@Finder("thumbnail")
|
||||
@RequiredArgsConstructor
|
||||
public class ThumbnailFinderImpl implements ThumbnailFinder {
|
||||
private final ThumbnailService thumbnailService;
|
||||
|
||||
@Override
|
||||
public Mono<String> gen(String uriStr, String size) {
|
||||
return Mono.fromSupplier(() -> URI.create(uriStr))
|
||||
.flatMap(uri -> thumbnailService.get(uri, ThumbnailSize.fromName(size)))
|
||||
.map(URI::toString)
|
||||
.onErrorResume(Throwable.class, e -> {
|
||||
log.debug("Failed to generate thumbnail for [{}], error: [{}]", uriStr,
|
||||
e.getMessage());
|
||||
return Mono.just(uriStr);
|
||||
})
|
||||
.defaultIfEmpty(uriStr);
|
||||
// TODO Implement me
|
||||
return Mono.just(uriStr);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,46 +0,0 @@
|
|||
package run.halo.app.content;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static run.halo.app.content.HtmlThumbnailSrcsetInjector.buildSizesAttr;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* Tests for {@link HtmlThumbnailSrcsetInjector}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.19.0
|
||||
*/
|
||||
class HtmlThumbnailSrcsetInjectorTest {
|
||||
|
||||
@Test
|
||||
void injectSrcset() {
|
||||
String html = """
|
||||
<div>
|
||||
<img src='image1.jpg' alt="test">
|
||||
<img src='image2.jpg' srcset='image2-small.jpg \
|
||||
480w, image2-large.jpg 800w'>
|
||||
</div>
|
||||
""";
|
||||
var result = HtmlThumbnailSrcsetInjector.injectSrcset(html,
|
||||
src -> Mono.just(src + " 480w, " + src + " 800w")).block();
|
||||
assertThat(result).isEqualToIgnoringWhitespace("""
|
||||
<div>
|
||||
<img src="image1.jpg" alt="test" srcset="image1.jpg 480w, image1.jpg 800w"\
|
||||
sizes="(max-width: 400px) 400px, (max-width: 800px) 800px,\
|
||||
(max-width: 1200px) 1200px, (max-width: 1600px) 1600px">
|
||||
<img src="image2.jpg" srcset="image2-small.jpg 480w, image2-large.jpg 800w">
|
||||
</div>
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildSizesTest() {
|
||||
var sizes = buildSizesAttr();
|
||||
assertThat(sizes).isEqualToIgnoringWhitespace("""
|
||||
(max-width: 400px) 400px, (max-width: 800px) 800px,
|
||||
(max-width: 1200px) 1200px, (max-width: 1600px) 1600px
|
||||
""");
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue