mirror of https://github.com/halo-dev/halo
Add thumbnail routing with dynamic width handling and URI generation
parent
0a18d9ef20
commit
68e80d16ed
|
@ -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();
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue