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.net.URI;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
import org.pf4j.ExtensionPoint;
|
import org.pf4j.ExtensionPoint;
|
||||||
import org.springframework.http.codec.multipart.FilePart;
|
import org.springframework.http.codec.multipart.FilePart;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
@ -69,6 +70,17 @@ public interface AttachmentHandler extends ExtensionPoint {
|
||||||
return Mono.empty();
|
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 {
|
interface UploadContext {
|
||||||
|
|
||||||
FilePart file();
|
FilePart file();
|
||||||
|
|
|
@ -100,7 +100,10 @@ class ThumbnailImgTagPostProcessor implements ElementTagPostProcessor {
|
||||||
var modelFactory = context.getModelFactory();
|
var modelFactory = context.getModelFactory();
|
||||||
if (!tag.hasAttribute("sizes")) {
|
if (!tag.hasAttribute("sizes")) {
|
||||||
tag = modelFactory.setAttribute(tag, "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()
|
var srcset = thumbnails.keySet().stream()
|
||||||
|
|
|
@ -51,12 +51,11 @@ public enum ThumbnailUtils {
|
||||||
return Map.of();
|
return Map.of();
|
||||||
}
|
}
|
||||||
return Arrays.stream(ThumbnailSize.values())
|
return Arrays.stream(ThumbnailSize.values())
|
||||||
.collect(Collectors.toMap(t -> t, t -> {
|
.collect(Collectors.toMap(t -> t, t ->
|
||||||
var prefix = "/thumbnails/w" + t.getWidth();
|
UriComponentsBuilder.fromUri(permalink)
|
||||||
return UriComponentsBuilder.fromUri(permalink)
|
.queryParam("width", t.getWidth())
|
||||||
.replacePath(prefix + permalink.getPath())
|
|
||||||
.build()
|
.build()
|
||||||
.toUri();
|
.toUri()
|
||||||
}));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package run.halo.app.core.attachment.endpoint;
|
package run.halo.app.core.attachment.endpoint;
|
||||||
|
|
||||||
import static java.nio.file.StandardOpenOption.READ;
|
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.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
|
@ -13,6 +15,7 @@ import org.apache.commons.io.FilenameUtils;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.core.io.FileSystemResource;
|
import org.springframework.core.io.FileSystemResource;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||||
import org.springframework.web.reactive.function.server.RouterFunctions;
|
import org.springframework.web.reactive.function.server.RouterFunctions;
|
||||||
|
@ -29,6 +32,8 @@ import run.halo.app.core.attachment.ThumbnailUtils;
|
||||||
@Component
|
@Component
|
||||||
class ThumbnailRouters {
|
class ThumbnailRouters {
|
||||||
|
|
||||||
|
private static final MediaType IMAGE_MEDIA_TYPE = MediaType.parseMediaType("image/*");
|
||||||
|
|
||||||
private final Path uploadRoot;
|
private final Path uploadRoot;
|
||||||
|
|
||||||
private final Path thumbnailRoot;
|
private final Path thumbnailRoot;
|
||||||
|
@ -49,46 +54,50 @@ class ThumbnailRouters {
|
||||||
@Bean
|
@Bean
|
||||||
RouterFunction<ServerResponse> thumbnailRouter() {
|
RouterFunction<ServerResponse> thumbnailRouter() {
|
||||||
return RouterFunctions.route()
|
return RouterFunctions.route()
|
||||||
.GET("/thumbnails/w{width}/upload/{*filename}", serverRequest -> {
|
.GET(
|
||||||
var width = serverRequest.pathVariable("width");
|
"/upload/{*filename}",
|
||||||
String originalFilename = serverRequest.pathVariable("filename");
|
queryParam("width", StringUtils::isNotBlank).and(accept(IMAGE_MEDIA_TYPE)),
|
||||||
var filename = StringUtils.removeStart(originalFilename, "/");
|
serverRequest -> {
|
||||||
if (StringUtils.isBlank(filename)) {
|
var size = serverRequest.queryParam("width")
|
||||||
log.trace("Filename is blank");
|
.map(ThumbnailSize::fromWidth)
|
||||||
return Mono.error(new NoResourceFoundException(filename));
|
.orElse(ThumbnailSize.M);
|
||||||
}
|
String originalFilename = serverRequest.pathVariable("filename");
|
||||||
var size = ThumbnailSize.fromWidth(width);
|
var filename = StringUtils.removeStart(originalFilename, "/");
|
||||||
// try to resolve the thumbnail
|
if (StringUtils.isBlank(filename)) {
|
||||||
// build thumbnail path
|
log.trace("Filename is blank");
|
||||||
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()) {
|
|
||||||
return Mono.error(new NoResourceFoundException(filename));
|
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
|
var uploadPath = uploadRoot.resolve(filename);
|
||||||
return Mono.fromCallable(
|
var fileSuffix = FilenameUtils.getExtension(filename);
|
||||||
() -> generateThumbnail(uploadPath, thumbnailPath, size)
|
if (!ThumbnailUtils.isSupportedImage(fileSuffix)) {
|
||||||
)
|
log.warn("File suffix {} is not supported for thumbnail generation, return "
|
||||||
.subscribeOn(this.thumbnailGeneratingScheduler)
|
+ "original file", fileSuffix);
|
||||||
.switchIfEmpty(Mono.error(() -> new NoResourceFoundException(filename)))
|
// return the original file
|
||||||
.map(FileSystemResource::new)
|
thumbnailResource = new FileSystemResource(uploadPath);
|
||||||
.flatMap(resource -> ServerResponse.ok().bodyValue(resource));
|
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();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue