Add thumbnail routing with dynamic width handling and URI generation

feat/add-thumbnail-router
John Niang 2025-09-28 15:50:15 +08:00
parent 0a18d9ef20
commit 68e80d16ed
No known key found for this signature in database
GPG Key ID: D7363C015BBCAA59
4 changed files with 67 additions and 44 deletions

View File

@ -3,6 +3,7 @@ package run.halo.app.core.extension.attachment.endpoint;
import java.net.URI;
import java.time.Duration;
import java.util.Map;
import java.util.Optional;
import org.pf4j.ExtensionPoint;
import org.springframework.http.codec.multipart.FilePart;
import reactor.core.publisher.Mono;
@ -69,6 +70,17 @@ public interface AttachmentHandler extends ExtensionPoint {
return Mono.empty();
}
/**
* Build the thumbnail links from the given permalink.
*
* @param permalink the permalink
* @return an optional map of thumbnail sizes to their respective URIs
*/
default Optional<Map<ThumbnailSize, URI>> buildThumbnailLinks(URI permalink) {
// resolve the thumbnail link
return Optional.empty();
}
interface UploadContext {
FilePart file();

View File

@ -100,7 +100,10 @@ class ThumbnailImgTagPostProcessor implements ElementTagPostProcessor {
var modelFactory = context.getModelFactory();
if (!tag.hasAttribute("sizes")) {
tag = modelFactory.setAttribute(tag, "sizes", """
(min-width: 800px) 800px, 100vw\
(max-width: 640px) 94vw, \
(max-width: 768px) 92vw, \
(max-width: 1024px) 88vw, \
min(800px, 85vw)\
""");
}
var srcset = thumbnails.keySet().stream()

View File

@ -51,12 +51,11 @@ public enum ThumbnailUtils {
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())
.collect(Collectors.toMap(t -> t, t ->
UriComponentsBuilder.fromUri(permalink)
.queryParam("width", t.getWidth())
.build()
.toUri();
}));
.toUri()
));
}
}

View File

@ -1,6 +1,8 @@
package run.halo.app.core.attachment.endpoint;
import static java.nio.file.StandardOpenOption.READ;
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
import static org.springframework.web.reactive.function.server.RequestPredicates.queryParam;
import java.io.IOException;
import java.nio.file.Files;
@ -13,6 +15,7 @@ import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.FileSystemResource;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
@ -29,6 +32,8 @@ import run.halo.app.core.attachment.ThumbnailUtils;
@Component
class ThumbnailRouters {
private static final MediaType IMAGE_MEDIA_TYPE = MediaType.parseMediaType("image/*");
private final Path uploadRoot;
private final Path thumbnailRoot;
@ -49,46 +54,50 @@ class ThumbnailRouters {
@Bean
RouterFunction<ServerResponse> thumbnailRouter() {
return RouterFunctions.route()
.GET("/thumbnails/w{width}/upload/{*filename}", serverRequest -> {
var width = serverRequest.pathVariable("width");
String originalFilename = serverRequest.pathVariable("filename");
var filename = StringUtils.removeStart(originalFilename, "/");
if (StringUtils.isBlank(filename)) {
log.trace("Filename is blank");
return Mono.error(new NoResourceFoundException(filename));
}
var size = ThumbnailSize.fromWidth(width);
// try to resolve the thumbnail
// build thumbnail path
var thumbnailPath = thumbnailRoot.resolve("w" + size.getWidth())
.resolve(filename);
var thumbnailResource = new FileSystemResource(thumbnailPath);
if (thumbnailResource.isReadable()) {
return ServerResponse.ok().bodyValue(thumbnailResource);
}
var uploadPath = uploadRoot.resolve(filename);
var fileSuffix = FilenameUtils.getExtension(filename);
if (!ThumbnailUtils.isSupportedImage(fileSuffix)) {
log.warn("File suffix {} is not supported for thumbnail generation, return "
+ "original file", fileSuffix);
// return the original file
thumbnailResource = new FileSystemResource(uploadPath);
if (!thumbnailResource.isReadable()) {
.GET(
"/upload/{*filename}",
queryParam("width", StringUtils::isNotBlank).and(accept(IMAGE_MEDIA_TYPE)),
serverRequest -> {
var size = serverRequest.queryParam("width")
.map(ThumbnailSize::fromWidth)
.orElse(ThumbnailSize.M);
String originalFilename = serverRequest.pathVariable("filename");
var filename = StringUtils.removeStart(originalFilename, "/");
if (StringUtils.isBlank(filename)) {
log.trace("Filename is blank");
return Mono.error(new NoResourceFoundException(filename));
}
return ServerResponse.ok().bodyValue(thumbnailResource);
}
// try to resolve the thumbnail
// build thumbnail path
var thumbnailPath = thumbnailRoot.resolve("w" + size.getWidth())
.resolve(filename);
var thumbnailResource = new FileSystemResource(thumbnailPath);
if (thumbnailResource.isReadable()) {
return ServerResponse.ok().bodyValue(thumbnailResource);
}
// generate for the attachment
return Mono.fromCallable(
() -> generateThumbnail(uploadPath, thumbnailPath, size)
)
.subscribeOn(this.thumbnailGeneratingScheduler)
.switchIfEmpty(Mono.error(() -> new NoResourceFoundException(filename)))
.map(FileSystemResource::new)
.flatMap(resource -> ServerResponse.ok().bodyValue(resource));
})
var uploadPath = uploadRoot.resolve(filename);
var fileSuffix = FilenameUtils.getExtension(filename);
if (!ThumbnailUtils.isSupportedImage(fileSuffix)) {
log.warn("File suffix {} is not supported for thumbnail generation, return "
+ "original file", fileSuffix);
// return the original file
thumbnailResource = new FileSystemResource(uploadPath);
if (!thumbnailResource.isReadable()) {
return Mono.error(new NoResourceFoundException(filename));
}
return ServerResponse.ok().bodyValue(thumbnailResource);
}
// generate for the attachment
return Mono.fromCallable(
() -> generateThumbnail(uploadPath, thumbnailPath, size)
)
.subscribeOn(this.thumbnailGeneratingScheduler)
.switchIfEmpty(Mono.error(() -> new NoResourceFoundException(filename)))
.map(FileSystemResource::new)
.flatMap(resource -> ServerResponse.ok().bodyValue(resource));
})
.build();
}