diff --git a/api/src/main/java/run/halo/app/core/extension/attachment/endpoint/AttachmentHandler.java b/api/src/main/java/run/halo/app/core/extension/attachment/endpoint/AttachmentHandler.java index 38369f7a8..fb031eadf 100644 --- a/api/src/main/java/run/halo/app/core/extension/attachment/endpoint/AttachmentHandler.java +++ b/api/src/main/java/run/halo/app/core/extension/attachment/endpoint/AttachmentHandler.java @@ -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> 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(); diff --git a/api/src/main/java/run/halo/app/core/extension/service/AttachmentService.java b/api/src/main/java/run/halo/app/core/extension/service/AttachmentService.java index b416f470c..3aedf8942 100644 --- a/api/src/main/java/run/halo/app/core/extension/service/AttachmentService.java +++ b/api/src/main/java/run/halo/app/core/extension/service/AttachmentService.java @@ -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 getSharedURL(Attachment attachment, Duration ttl); + Mono> getThumbnailLinks(Attachment attachment); + /** * Transfer external links to attachments. * diff --git a/api/src/main/java/run/halo/app/extension/controller/DefaultController.java b/api/src/main/java/run/halo/app/extension/controller/DefaultController.java index 7253ecb9d..1dbd293e9 100644 --- a/api/src/main/java/run/halo/app/extension/controller/DefaultController.java +++ b/api/src/main/java/run/halo/app/extension/controller/DefaultController.java @@ -188,6 +188,8 @@ public class DefaultController 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 diff --git a/api/src/main/java/run/halo/app/extension/exception/NotImplementedException.java b/api/src/main/java/run/halo/app/extension/exception/NotImplementedException.java new file mode 100644 index 000000000..3535e0c84 --- /dev/null +++ b/api/src/main/java/run/halo/app/extension/exception/NotImplementedException.java @@ -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); + } + +} diff --git a/application/src/main/java/run/halo/app/core/attachment/endpoint/LocalAttachmentUploadHandler.java b/application/src/main/java/run/halo/app/core/attachment/endpoint/LocalAttachmentUploadHandler.java index f7c8fd60c..e29122a20 100644 --- a/application/src/main/java/run/halo/app/core/attachment/endpoint/LocalAttachmentUploadHandler.java +++ b/application/src/main/java/run/halo/app/core/attachment/endpoint/LocalAttachmentUploadHandler.java @@ -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> 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 diff --git a/application/src/main/java/run/halo/app/core/attachment/reconciler/AttachmentReconciler.java b/application/src/main/java/run/halo/app/core/attachment/reconciler/AttachmentReconciler.java index cc99fb226..d8a700378 100644 --- a/application/src/main/java/run/halo/app/core/attachment/reconciler/AttachmentReconciler.java +++ b/application/src/main/java/run/halo/app/core/attachment/reconciler/AttachmentReconciler.java @@ -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 { 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 { .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()) diff --git a/application/src/main/java/run/halo/app/core/user/service/impl/DefaultAttachmentService.java b/application/src/main/java/run/halo/app/core/user/service/impl/DefaultAttachmentService.java index c79bdd934..57302ca35 100644 --- a/application/src/main/java/run/halo/app/core/user/service/impl/DefaultAttachmentService.java +++ b/application/src/main/java/run/halo/app/core/user/service/impl/DefaultAttachmentService.java @@ -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> 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 uploadFromUrl(@NonNull URL url, @NonNull String policyName, String groupName, String filename) { diff --git a/application/src/test/java/run/halo/app/PathPrefixPredicateTest.java b/application/src/test/java/run/halo/app/PathPrefixPredicateTest.java index 6ca99b356..3afa8339c 100644 --- a/application/src/test/java/run/halo/app/PathPrefixPredicateTest.java +++ b/application/src/test/java/run/halo/app/PathPrefixPredicateTest.java @@ -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("/")); } }