Implement thumbnail link retrieval in attachment handling

feat/add-thumbnail-router
John Niang 2025-09-24 11:06:57 +08:00
parent 503e9b8c7f
commit 8f198d9583
No known key found for this signature in database
GPG Key ID: D7363C015BBCAA59
8 changed files with 141 additions and 55 deletions

View File

@ -2,12 +2,15 @@ package run.halo.app.core.extension.attachment.endpoint;
import java.net.URI;
import java.time.Duration;
import java.util.Map;
import org.pf4j.ExtensionPoint;
import org.springframework.http.codec.multipart.FilePart;
import reactor.core.publisher.Mono;
import run.halo.app.core.attachment.ThumbnailSize;
import run.halo.app.core.extension.attachment.Attachment;
import run.halo.app.core.extension.attachment.Policy;
import run.halo.app.extension.ConfigMap;
import run.halo.app.extension.exception.NotImplementedException;
public interface AttachmentHandler extends ExtensionPoint {
@ -53,6 +56,24 @@ public interface AttachmentHandler extends ExtensionPoint {
return Mono.empty();
}
/**
* Gets thumbnail links for given attachment.
* The default implementation will raise NotImplementedException.
*
* @param attachment the attachment
* @param policy the policy
* @param configMap the config map
* @return a map of thumbnail sizes to their respective URIs
*/
default Mono<Map<ThumbnailSize, URI>> getThumbnailLinks(Attachment attachment,
Policy policy,
ConfigMap configMap) {
return Mono.error(new NotImplementedException(
"getThumbnailLinks method is not implemented for " + attachment.getMetadata().getName()
+ ", please try to upgrade the corresponding plugin"
));
}
interface UploadContext {
FilePart file();

View File

@ -3,6 +3,7 @@ package run.halo.app.core.extension.service;
import java.net.URI;
import java.net.URL;
import java.time.Duration;
import java.util.Map;
import java.util.function.Consumer;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.MediaType;
@ -11,6 +12,7 @@ import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import run.halo.app.core.attachment.ThumbnailSize;
import run.halo.app.core.extension.attachment.Attachment;
/**
@ -92,6 +94,8 @@ public interface AttachmentService {
*/
Mono<URI> getSharedURL(Attachment attachment, Duration ttl);
Mono<Map<ThumbnailSize, URI>> getThumbnailLinks(Attachment attachment);
/**
* Transfer external links to attachments.
*

View File

@ -188,6 +188,8 @@ public class DefaultController<R> implements Controller {
log.warn("Optimistic locking failure when reconciling request: {}/{}",
this.name, entry.getEntry());
} else if (t instanceof RequeueException re) {
log.warn("{}: Requeue {} due to {}",
this.name, entry.getEntry(), re.getMessage());
result = re.getResult();
} else {
log.error("Reconciler in " + this.name

View File

@ -0,0 +1,18 @@
package run.halo.app.extension.exception;
/**
* Exception thrown to indicate that the requested operation is not implemented.
*
* @author johnniang
*/
public class NotImplementedException extends UnsupportedOperationException {
public NotImplementedException() {
this("Not implemented");
}
public NotImplementedException(String message) {
super(message);
}
}

View File

@ -15,11 +15,13 @@ import java.nio.file.Path;
import java.time.Clock;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Map;
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;
@ -42,6 +44,7 @@ import reactor.core.publisher.SynchronousSink;
import reactor.core.scheduler.Schedulers;
import reactor.util.retry.Retry;
import run.halo.app.core.attachment.AttachmentRootGetter;
import run.halo.app.core.attachment.ThumbnailSize;
import run.halo.app.core.extension.attachment.Attachment;
import run.halo.app.core.extension.attachment.Attachment.AttachmentSpec;
import run.halo.app.core.extension.attachment.Constant;
@ -240,6 +243,25 @@ class LocalAttachmentUploadHandler implements AttachmentHandler {
} else {
log.info("{} was not exist", attachment);
}
// TODO Delete thumbnails if present
var thumbnailsRoot = attachmentsRoot.resolve("thumbnails");
var relativeAttachmentPath =
attachmentsRoot.resolve("upload").relativize(attachmentPath);
log.info("Clean up thumbnails for {}", localRelativePath);
Arrays.stream(ThumbnailSize.values())
.forEach(thumbnailSize -> {
var thumbnailPath =
thumbnailsRoot.resolve("w" + thumbnailSize.getWidth())
.resolve(relativeAttachmentPath);
try {
Files.deleteIfExists(thumbnailPath);
log.info("Deleted thumbnail {}", thumbnailPath);
} catch (IOException e) {
// ignore the exception to continue deleting other
// thumbnails
log.warn("Cannot delete thumbnail {}", thumbnailPath, e);
}
});
} catch (IOException e) {
throw Exceptions.propagate(e);
}
@ -278,6 +300,28 @@ class LocalAttachmentUploadHandler implements AttachmentHandler {
return getPermalink(attachment, policy, configMap);
}
@Override
public Mono<Map<ThumbnailSize, URI>> getThumbnailLinks(Attachment attachment, Policy policy,
ConfigMap configMap) {
if (!this.shouldHandle(policy)) {
return Mono.empty();
}
if (attachment.getStatus() == null
|| !StringUtils.hasText(attachment.getStatus().getPermalink())) {
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();
}));
return Mono.just(thumbnails);
}
private boolean shouldHandle(Policy policy) {
if (policy == null
|| policy.getSpec() == null

View File

@ -5,18 +5,13 @@ import static run.halo.app.extension.ExtensionUtil.removeFinalizers;
import java.net.URI;
import java.time.Duration;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import run.halo.app.core.attachment.AttachmentUtils;
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.attachment.Attachment.AttachmentStatus;
import run.halo.app.core.extension.attachment.Constant;
@ -28,6 +23,7 @@ import run.halo.app.extension.controller.ControllerBuilder;
import run.halo.app.extension.controller.Reconciler;
import run.halo.app.extension.controller.Reconciler.Request;
import run.halo.app.extension.controller.RequeueException;
import run.halo.app.extension.exception.NotImplementedException;
@Slf4j
@Component
@ -44,47 +40,51 @@ public class AttachmentReconciler implements Reconciler<Request> {
public Result reconcile(Request request) {
client.fetch(Attachment.class, request.name()).ifPresent(attachment -> {
if (ExtensionUtil.isDeleted(attachment)) {
if (removeFinalizers(attachment.getMetadata(), Set.of(Constant.FINALIZER_NAME))) {
if (removeFinalizers(attachment.getMetadata(),
Set.of(Constant.FINALIZER_NAME))) {
cleanUpResources(attachment);
client.update(attachment);
}
return;
}
// add finalizer
if (addFinalizers(attachment.getMetadata(), Set.of(Constant.FINALIZER_NAME))) {
client.update(attachment);
}
addFinalizers(attachment.getMetadata(), Set.of(Constant.FINALIZER_NAME));
var annotations = attachment.getMetadata().getAnnotations();
if (annotations != null) {
var permalink = attachmentService.getPermalink(attachment)
.map(URI::toASCIIString)
.blockOptional()
.orElseThrow(() -> new RequeueException(new Result(true, null),
"Attachment handler is unavailable, requeue the request"
));
log.debug("Set permalink {} for attachment {}", permalink, request.name());
var status = nullSafeStatus(attachment);
status.setPermalink(permalink);
if (attachment.getStatus() == null) {
attachment.setStatus(new AttachmentStatus());
}
var permalink = nullSafeStatus(attachment).getPermalink();
if (StringUtils.isNotBlank(permalink) && AttachmentUtils.isImage(attachment)) {
populateThumbnails(permalink, attachment.getStatus());
}
updateStatus(request.name(), attachment.getStatus());
var permalink = attachmentService.getPermalink(attachment)
.map(URI::toASCIIString)
.blockOptional(Duration.ofSeconds(10))
.orElseThrow(() -> new RequeueException(new Result(true, null),
"Attachment handler is unavailable, requeue the request"
));
log.debug("Set attachment permalink: {} for {}", permalink, request.name());
attachment.getStatus().setPermalink(permalink);
var thumbnails = attachmentService.getThumbnailLinks(attachment)
.map(map -> map.keySet()
.stream()
.collect(Collectors.toMap(Enum::name, k -> map.get(k).toASCIIString()))
)
.onErrorMap(NotImplementedException.class,
e -> new RequeueException(new Result(true, null),
"Attachment handler does not implement thumbnail generation, requeue "
+ "the "
+ "request"
))
.blockOptional(Duration.ofSeconds(10))
.orElseThrow(() -> new RequeueException(new Result(true, null), """
Attachment handler is unavailable for getting thumbnails links, \
requeue the request\
"""
));
attachment.getStatus().setThumbnails(thumbnails);
log.debug("Set attachment thumbnails: {} for {}", thumbnails, request.name());
client.update(attachment);
});
return null;
}
private static AttachmentStatus nullSafeStatus(Attachment attachment) {
var status = attachment.getStatus();
if (status == null) {
status = new AttachmentStatus();
attachment.setStatus(status);
}
return status;
}
@Override
public Controller setupWith(ControllerBuilder builder) {
return builder
@ -92,26 +92,6 @@ public class AttachmentReconciler implements Reconciler<Request> {
.build();
}
void populateThumbnails(String permalink, AttachmentStatus status) {
var imageUri = URI.create(permalink);
Flux.fromArray(ThumbnailSize.values())
.flatMap(size -> thumbnailService.generate(imageUri, size)
.map(thumbUri -> Map.entry(size.name(), thumbUri.toString()))
)
.collectMap(Map.Entry::getKey, Map.Entry::getValue)
.doOnNext(status::setThumbnails)
.block();
}
void updateStatus(String attachmentName, AttachmentStatus status) {
client.fetch(Attachment.class, attachmentName)
.filter(attachment -> !Objects.deepEquals(attachment.getStatus(), status))
.ifPresent(attachment -> {
attachment.setStatus(status);
client.update(attachment);
});
}
void cleanUpResources(Attachment attachment) {
var timeout = Duration.ofSeconds(20);
Optional.ofNullable(attachment.getStatus())

View File

@ -4,6 +4,7 @@ import java.net.URI;
import java.net.URL;
import java.nio.file.Paths;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Function;
@ -24,6 +25,7 @@ import org.springframework.web.server.ServerErrorException;
import org.springframework.web.server.ServerWebInputException;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import run.halo.app.core.attachment.ThumbnailSize;
import run.halo.app.core.extension.attachment.Attachment;
import run.halo.app.core.extension.attachment.Policy;
import run.halo.app.core.extension.attachment.endpoint.AttachmentHandler;
@ -139,6 +141,19 @@ public class DefaultAttachmentService implements AttachmentService {
);
}
@Override
public Mono<Map<ThumbnailSize, URI>> getThumbnailLinks(Attachment attachment) {
return client.get(Policy.class, attachment.getSpec().getPolicyName())
.zipWhen(policy -> client.get(ConfigMap.class, policy.getSpec().getConfigMapName()))
.flatMap(tuple2 -> {
var policy = tuple2.getT1();
var configMap = tuple2.getT2();
return extensionGetter.getExtensions(AttachmentHandler.class)
.concatMap(handler -> handler.getThumbnailLinks(attachment, policy, configMap))
.next();
});
}
@Override
public Mono<Attachment> uploadFromUrl(@NonNull URL url, @NonNull String policyName,
String groupName, String filename) {

View File

@ -40,5 +40,7 @@ public class PathPrefixPredicateTest {
void urlTest() {
URI uri = URI.create("https:///path");
System.out.println(uri);
System.out.println(uri.getPath());
System.out.println(URI.create("/"));
}
}