mirror of https://github.com/halo-dev/halo
Implement thumbnail link retrieval in attachment handling
parent
503e9b8c7f
commit
8f198d9583
|
@ -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();
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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("/"));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue