From 36d926709748885e3214818a58e916b5c651b886 Mon Sep 17 00:00:00 2001
From: guqing
Date: Mon, 12 Aug 2024 16:35:50 +0800
Subject: [PATCH 1/4] feat: ability to configure and use image thumbnails
---
build.gradle | 2 +-
.../s3os/AttachmentThumbnailReconciler.java | 74 ++++++++++
.../java/run/halo/s3os/S3LinkServiceImpl.java | 4 +-
.../run/halo/s3os/S3OsAttachmentHandler.java | 49 ++-----
src/main/java/run/halo/s3os/S3OsPlugin.java | 1 +
.../java/run/halo/s3os/S3OsProperties.java | 42 +++++-
.../run/halo/s3os/S3ThumbnailProvider.java | 128 ++++++++++++++++++
.../resources/extensions/ext-definitions.yaml | 9 ++
.../extensions/policy-template-s3os.yaml | 43 +++++-
.../AttachmentThumbnailReconcilerTest.java | 25 ++++
10 files changed, 326 insertions(+), 51 deletions(-)
create mode 100644 src/main/java/run/halo/s3os/AttachmentThumbnailReconciler.java
create mode 100644 src/main/java/run/halo/s3os/S3ThumbnailProvider.java
create mode 100644 src/main/resources/extensions/ext-definitions.yaml
create mode 100644 src/test/java/run/halo/s3os/AttachmentThumbnailReconcilerTest.java
diff --git a/build.gradle b/build.gradle
index 92bcde0..af92100 100644
--- a/build.gradle
+++ b/build.gradle
@@ -20,7 +20,7 @@ repositories {
}
dependencies {
- implementation platform('run.halo.tools.platform:plugin:2.17.0-SNAPSHOT')
+ implementation platform('run.halo.tools.platform:plugin:2.19.0-SNAPSHOT')
compileOnly 'run.halo.app:api'
implementation platform('software.amazon.awssdk:bom:2.19.8')
diff --git a/src/main/java/run/halo/s3os/AttachmentThumbnailReconciler.java b/src/main/java/run/halo/s3os/AttachmentThumbnailReconciler.java
new file mode 100644
index 0000000..d662174
--- /dev/null
+++ b/src/main/java/run/halo/s3os/AttachmentThumbnailReconciler.java
@@ -0,0 +1,74 @@
+package run.halo.s3os;
+
+import static run.halo.app.infra.FileCategoryMatcher.IMAGE;
+
+import java.time.Duration;
+import java.time.Instant;
+import lombok.RequiredArgsConstructor;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Component;
+import org.springframework.util.Assert;
+import run.halo.app.core.extension.attachment.Attachment;
+import run.halo.app.extension.ExtensionClient;
+import run.halo.app.extension.MetadataUtil;
+import run.halo.app.extension.controller.Controller;
+import run.halo.app.extension.controller.ControllerBuilder;
+import run.halo.app.extension.controller.Reconciler;
+
+/**
+ *
This {@link Reconciler} used to check thumbnail status are generated
+ * if not, update the attachment to trigger thumbnail generation by halo
+ */
+@Component
+@RequiredArgsConstructor
+public class AttachmentThumbnailReconciler implements Reconciler {
+ static final String REQUEST_GEN_THUMBNAIL = "s3os.halo.run/request-gen-thumbnail";
+ private final ExtensionClient client;
+
+ @Override
+ public Result reconcile(Request request) {
+ client.fetch(Attachment.class, request.name())
+ .ifPresent(attachment -> {
+ var annotations = MetadataUtil.nullSafeAnnotations(attachment);
+ var requestTime = annotations.get(REQUEST_GEN_THUMBNAIL);
+ if (isMadeWithIn1Day(requestTime)) {
+ // skip if request is made within 1 day
+ return;
+ }
+
+ if (!isImage(attachment)) {
+ // skip non-image attachments
+ return;
+ }
+
+ var status = attachment.getStatus();
+ if (status == null || status.getThumbnails() == null) {
+ // update to trigger attachment thumbnail generation
+ annotations.put(REQUEST_GEN_THUMBNAIL, Instant.now().toString());
+ client.update(attachment);
+ }
+ });
+ return Result.doNotRetry();
+ }
+
+ static boolean isMadeWithIn1Day(String requestTimeStr) {
+ if (StringUtils.isBlank(requestTimeStr)) {
+ return false;
+ }
+ var requestTime = Instant.parse(requestTimeStr);
+ return Duration.between(requestTime, Instant.now()).minusDays(1).isNegative();
+ }
+
+ public static boolean isImage(Attachment attachment) {
+ Assert.notNull(attachment, "Attachment must not be null");
+ var mediaType = attachment.getSpec().getMediaType();
+ return mediaType != null && IMAGE.match(mediaType);
+ }
+
+ @Override
+ public Controller setupWith(ControllerBuilder builder) {
+ return builder
+ .extension(new Attachment())
+ .build();
+ }
+}
diff --git a/src/main/java/run/halo/s3os/S3LinkServiceImpl.java b/src/main/java/run/halo/s3os/S3LinkServiceImpl.java
index ba6b6d1..8e81459 100644
--- a/src/main/java/run/halo/s3os/S3LinkServiceImpl.java
+++ b/src/main/java/run/halo/s3os/S3LinkServiceImpl.java
@@ -67,7 +67,7 @@ public Mono listObjects(String policyName, String continuationToke
return client.fetch(ConfigMap.class, configMapName);
})
.flatMap((configMap) -> {
- var properties = handler.getProperties(configMap);
+ var properties = S3OsProperties.convertFrom(configMap);
var finalLocation = FilePathUtils.getFilePathByPlaceholder(properties.getLocation());
return Mono.using(() -> handler.buildS3Client(properties),
// 执行 listObjects
@@ -231,7 +231,7 @@ public Mono addAttachmentRecord(String policyName,
return client.fetch(ConfigMap.class, configMapName);
})
.flatMap(configMap -> {
- var properties = handler.getProperties(configMap);
+ var properties = S3OsProperties.convertFrom(configMap);
return Mono.using(() -> handler.buildS3Client(properties),
(s3Client) -> Mono.fromCallable(
() -> s3Client.headObject(
diff --git a/src/main/java/run/halo/s3os/S3OsAttachmentHandler.java b/src/main/java/run/halo/s3os/S3OsAttachmentHandler.java
index c2db9c3..ca57fb1 100644
--- a/src/main/java/run/halo/s3os/S3OsAttachmentHandler.java
+++ b/src/main/java/run/halo/s3os/S3OsAttachmentHandler.java
@@ -3,7 +3,6 @@
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
-import java.nio.charset.StandardCharsets;
import java.nio.file.FileAlreadyExistsException;
import java.time.Duration;
import java.util.HashMap;
@@ -23,7 +22,6 @@
import org.springframework.lang.Nullable;
import org.springframework.web.server.ServerErrorException;
import org.springframework.web.server.ServerWebInputException;
-import org.springframework.web.util.UriUtils;
import reactor.core.Exceptions;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -38,7 +36,6 @@
import run.halo.app.extension.ConfigMap;
import run.halo.app.extension.Metadata;
import run.halo.app.extension.MetadataUtil;
-import run.halo.app.infra.utils.JsonUtils;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.awscore.presigner.SdkPresigner;
import software.amazon.awssdk.core.SdkResponse;
@@ -47,16 +44,7 @@
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.S3Configuration;
-import software.amazon.awssdk.services.s3.model.CompleteMultipartUploadRequest;
-import software.amazon.awssdk.services.s3.model.CompletedMultipartUpload;
-import software.amazon.awssdk.services.s3.model.CompletedPart;
-import software.amazon.awssdk.services.s3.model.CreateMultipartUploadRequest;
-import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
-import software.amazon.awssdk.services.s3.model.GetObjectRequest;
-import software.amazon.awssdk.services.s3.model.HeadObjectRequest;
-import software.amazon.awssdk.services.s3.model.HeadObjectResponse;
-import software.amazon.awssdk.services.s3.model.NoSuchKeyException;
-import software.amazon.awssdk.services.s3.model.UploadPartRequest;
+import software.amazon.awssdk.services.s3.model.*;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;
import software.amazon.awssdk.utils.SdkAutoCloseable;
@@ -79,7 +67,7 @@ public class S3OsAttachmentHandler implements AttachmentHandler {
public Mono upload(UploadContext uploadContext) {
return Mono.just(uploadContext).filter(context -> this.shouldHandle(context.policy()))
.flatMap(context -> {
- final var properties = getProperties(context.configMap());
+ final var properties = S3OsProperties.convertFrom(context.configMap());
return upload(context, properties)
.subscribeOn(Schedulers.boundedElastic())
.map(objectDetail -> this.buildAttachment(properties, objectDetail))
@@ -102,7 +90,7 @@ public Mono delete(DeleteContext deleteContext) {
log.info("Skip deleting object {} from S3.", objectKey);
return Mono.just(context);
}
- var properties = getProperties(deleteContext.configMap());
+ var properties = S3OsProperties.convertFrom(deleteContext.configMap());
return Mono.using(() -> buildS3Client(properties),
client -> Mono.fromCallable(
() -> client.deleteObject(DeleteObjectRequest.builder()
@@ -123,7 +111,7 @@ public Mono delete(DeleteContext deleteContext) {
@Override
public Mono getSharedURL(Attachment attachment, Policy policy, ConfigMap configMap,
- Duration ttl) {
+ Duration ttl) {
if (!this.shouldHandle(policy)) {
return Mono.empty();
}
@@ -132,7 +120,7 @@ public Mono getSharedURL(Attachment attachment, Policy policy, ConfigMap co
return Mono.error(new IllegalArgumentException(
"Cannot obtain object key from attachment " + attachment.getMetadata().getName()));
}
- var properties = getProperties(configMap);
+ var properties = S3OsProperties.convertFrom(configMap);
return Mono.using(() -> buildS3Presigner(properties),
s3Presigner -> {
@@ -168,8 +156,8 @@ public Mono getPermalink(Attachment attachment, Policy policy, ConfigMap co
// fallback to default handler for backward compatibility
return Mono.empty();
}
- var properties = getProperties(configMap);
- var objectURL = getObjectURL(properties, objectKey);
+ var properties = S3OsProperties.convertFrom(configMap);
+ var objectURL = properties.toObjectURL(objectKey);
var urlSuffix = getUrlSuffixAnnotation(attachment);
if (StringUtils.isNotBlank(urlSuffix)) {
objectURL += urlSuffix;
@@ -195,13 +183,8 @@ private String getUrlSuffixAnnotation(Attachment attachment) {
return annotations.get(URL_SUFFIX_ANNO_KEY);
}
- S3OsProperties getProperties(ConfigMap configMap) {
- var settingJson = configMap.getData().getOrDefault("default", "{}");
- return JsonUtils.jsonToObject(settingJson, S3OsProperties.class);
- }
-
Attachment buildAttachment(S3OsProperties properties, ObjectDetail objectDetail) {
- String externalLink = getObjectURL(properties, objectDetail.uploadState.objectKey);
+ String externalLink = properties.toObjectURL(objectDetail.uploadState.objectKey);
var urlSuffix = UrlUtils.findUrlSuffix(properties.getUrlSuffixes(),
objectDetail.uploadState.fileName);
@@ -229,22 +212,6 @@ Attachment buildAttachment(S3OsProperties properties, ObjectDetail objectDetail)
return attachment;
}
- String getObjectURL(S3OsProperties properties, String objectKey) {
- String objectURL;
- if (StringUtils.isBlank(properties.getDomain())) {
- String host;
- if (properties.getEnablePathStyleAccess()) {
- host = properties.getEndpoint() + "/" + properties.getBucket();
- } else {
- host = properties.getBucket() + "." + properties.getEndpoint();
- }
- objectURL = properties.getProtocol() + "://" + host + "/" + objectKey;
- } else {
- objectURL = properties.getProtocol() + "://" + properties.getDomain() + "/" + objectKey;
- }
- return UriUtils.encodePath(objectURL, StandardCharsets.UTF_8);
- }
-
S3Client buildS3Client(S3OsProperties properties) {
return S3Client.builder()
.region(Region.of(properties.getRegion()))
diff --git a/src/main/java/run/halo/s3os/S3OsPlugin.java b/src/main/java/run/halo/s3os/S3OsPlugin.java
index 7851027..5c3cdbd 100644
--- a/src/main/java/run/halo/s3os/S3OsPlugin.java
+++ b/src/main/java/run/halo/s3os/S3OsPlugin.java
@@ -10,6 +10,7 @@
*/
@Component
public class S3OsPlugin extends BasePlugin {
+ public static final String POLICY_SETTING_NAME = "s3os-policy-template-setting";
public S3OsPlugin(PluginContext pluginContext) {
super(pluginContext);
diff --git a/src/main/java/run/halo/s3os/S3OsProperties.java b/src/main/java/run/halo/s3os/S3OsProperties.java
index 634eea2..b87c7da 100644
--- a/src/main/java/run/halo/s3os/S3OsProperties.java
+++ b/src/main/java/run/halo/s3os/S3OsProperties.java
@@ -1,16 +1,17 @@
package run.halo.s3os;
+import java.nio.charset.StandardCharsets;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
-import lombok.RequiredArgsConstructor;
import org.springframework.util.StringUtils;
-
-import java.time.LocalDate;
+import org.springframework.web.util.UriUtils;
+import run.halo.app.extension.ConfigMap;
+import run.halo.app.infra.utils.JsonUtils;
@Data
-class S3OsProperties {
+public class S3OsProperties {
private String bucket;
@@ -49,6 +50,14 @@ class S3OsProperties {
private List urlSuffixes;
+ private ThumbnailParam thumbnailParam;
+
+ public record ThumbnailParam(String type, String pattern) {
+ public boolean hasPattern() {
+ return StringUtils.hasText(type) && StringUtils.hasText(pattern);
+ }
+ }
+
@Data
@AllArgsConstructor
@NoArgsConstructor
@@ -103,14 +112,14 @@ public void setRandomStringLength(String randomStringLength) { // if you use In
if (length >= 4 && length <= 16) {
this.randomStringLength = length;
}
+ } catch (NumberFormatException ignored) {
}
- catch (NumberFormatException ignored) { }
}
public void setRegion(String region) {
if (!StringUtils.hasText(region)) {
this.region = "Auto";
- }else {
+ } else {
this.region = region;
}
}
@@ -118,4 +127,25 @@ public void setRegion(String region) {
public void setEndpoint(String endpoint) {
this.endpoint = UrlUtils.removeHttpPrefix(endpoint);
}
+
+ public String toObjectURL(String objectKey) {
+ String objectURL;
+ if (!StringUtils.hasText(this.getDomain())) {
+ String host;
+ if (this.getEnablePathStyleAccess()) {
+ host = this.getEndpoint() + "/" + this.getBucket();
+ } else {
+ host = this.getBucket() + "." + this.getEndpoint();
+ }
+ objectURL = this.getProtocol() + "://" + host + "/" + objectKey;
+ } else {
+ objectURL = this.getProtocol() + "://" + this.getDomain() + "/" + objectKey;
+ }
+ return UriUtils.encodePath(objectURL, StandardCharsets.UTF_8);
+ }
+
+ public static S3OsProperties convertFrom(ConfigMap configMap) {
+ var settingJson = configMap.getData().getOrDefault("default", "{}");
+ return JsonUtils.jsonToObject(settingJson, S3OsProperties.class);
+ }
}
diff --git a/src/main/java/run/halo/s3os/S3ThumbnailProvider.java b/src/main/java/run/halo/s3os/S3ThumbnailProvider.java
new file mode 100644
index 0000000..0e6ac8c
--- /dev/null
+++ b/src/main/java/run/halo/s3os/S3ThumbnailProvider.java
@@ -0,0 +1,128 @@
+package run.halo.s3os;
+
+import static run.halo.s3os.S3OsPlugin.POLICY_SETTING_NAME;
+
+import java.net.URI;
+import java.net.URL;
+import java.util.List;
+import java.util.Map;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import lombok.Builder;
+import lombok.RequiredArgsConstructor;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.data.domain.Sort;
+import org.springframework.stereotype.Component;
+import org.springframework.util.Assert;
+import org.springframework.web.util.UriComponentsBuilder;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import run.halo.app.core.attachment.ThumbnailProvider;
+import run.halo.app.core.attachment.ThumbnailSize;
+import run.halo.app.core.extension.attachment.Policy;
+import run.halo.app.core.extension.attachment.PolicyTemplate;
+import run.halo.app.extension.ConfigMap;
+import run.halo.app.extension.ListOptions;
+import run.halo.app.extension.ReactiveExtensionClient;
+
+@Component
+@RequiredArgsConstructor
+public class S3ThumbnailProvider implements ThumbnailProvider {
+ static final String WIDTH_PLACEHOLDER = "{width}";
+ private final Cache s3PropsCache = CacheBuilder.newBuilder()
+ .maximumSize(50)
+ .build();
+
+ private final ReactiveExtensionClient client;
+
+ @Override
+ public Mono generate(ThumbnailContext thumbnailContext) {
+ var url = thumbnailContext.getImageUrl().toString();
+ var size = thumbnailContext.getSize();
+ return getCacheValue(url)
+ .mapNotNull(cacheValue -> placedPattern(cacheValue.pattern(), size))
+ .map(param -> {
+ if (param.startsWith("?")) {
+ UriComponentsBuilder.fromHttpUrl(url)
+ .queryParam(param.substring(1));
+ }
+ return url + param;
+ })
+ .map(URI::create);
+ }
+
+ private static String placedPattern(String pattern, ThumbnailSize size) {
+ return StringUtils.replace(pattern, WIDTH_PLACEHOLDER, String.valueOf(size.getWidth()));
+ }
+
+ @Override
+ public Mono delete(URL url) {
+ // do nothing for s3
+ return Mono.empty();
+ }
+
+ @Override
+ public Mono supports(ThumbnailContext thumbnailContext) {
+ var url = thumbnailContext.getImageUrl().toString();
+ return getCacheValue(url).hasElement();
+ }
+
+ private Mono getCacheValue(String imageUrl) {
+ return Flux.fromIterable(s3PropsCache.asMap().entrySet())
+ .filter(entry -> imageUrl.startsWith(entry.getKey()))
+ .next()
+ .map(Map.Entry::getValue)
+ .switchIfEmpty(Mono.defer(() -> listAllS3ObjectDomain()
+ .filter(entry -> imageUrl.startsWith(entry.getKey()))
+ .map(Map.Entry::getValue)
+ .next()
+ ));
+ }
+
+ @Builder
+ record S3PropsCacheValue(String pattern, String configMapName) {
+ }
+
+ private Flux> listAllS3ObjectDomain() {
+ return listS3PolicyTemplateNames()
+ .collectList()
+ .flatMapMany(this::listAllS3Policy)
+ .flatMap(s3Policy -> {
+ var s3ConfigMapName = s3Policy.getSpec().getConfigMapName();
+ return fetchS3PropsByConfigMapName(s3ConfigMapName)
+ .mapNotNull(properties -> {
+ var thumbnailParam = properties.getThumbnailParam();
+ if (thumbnailParam == null || !thumbnailParam.hasPattern()) {
+ return null;
+ }
+ var objectDomain = properties.toObjectURL("");
+ var cacheValue = S3PropsCacheValue.builder()
+ .pattern(thumbnailParam.pattern())
+ .configMapName(s3ConfigMapName)
+ .build();
+ return Map.entry(objectDomain, cacheValue);
+ });
+ })
+ .doOnNext(cache -> s3PropsCache.put(cache.getKey(), cache.getValue()));
+ }
+
+ private Flux listAllS3Policy(List s3PolicyTemplateNames) {
+ Assert.notNull(s3PolicyTemplateNames, "The s3PolicyTemplateNames must not be null.");
+ return client.listAll(Policy.class, new ListOptions(), Sort.unsorted())
+ .filter(policy -> {
+ var templateName = policy.getSpec().getTemplateName();
+ return s3PolicyTemplateNames.contains(templateName);
+ });
+ }
+
+ private Mono fetchS3PropsByConfigMapName(String name) {
+ return client.fetch(ConfigMap.class, name)
+ .map(S3OsProperties::convertFrom);
+ }
+
+ private Flux listS3PolicyTemplateNames() {
+ return client.listAll(PolicyTemplate.class, new ListOptions(), Sort.unsorted())
+ .filter(policyTemplate -> POLICY_SETTING_NAME.equals(policyTemplate.getSpec().getSettingName()))
+ .map(template -> template.getMetadata().getName());
+ }
+}
diff --git a/src/main/resources/extensions/ext-definitions.yaml b/src/main/resources/extensions/ext-definitions.yaml
new file mode 100644
index 0000000..fbc1d0c
--- /dev/null
+++ b/src/main/resources/extensions/ext-definitions.yaml
@@ -0,0 +1,9 @@
+apiVersion: plugin.halo.run/v1alpha1
+kind: ExtensionDefinition
+metadata:
+ name: s3os-thumbnail-provider
+spec:
+ className: run.halo.s3os.S3ThumbnailProvider
+ extensionPointName: thumbnail-provider
+ displayName: "S3 协议 OSS 缩略图生成"
+ description: "为上传到支持 S3 协议的 OSS 的图片生成缩略图"
diff --git a/src/main/resources/extensions/policy-template-s3os.yaml b/src/main/resources/extensions/policy-template-s3os.yaml
index 80c7c01..f44e4c5 100644
--- a/src/main/resources/extensions/policy-template-s3os.yaml
+++ b/src/main/resources/extensions/policy-template-s3os.yaml
@@ -144,4 +144,45 @@ spec:
name: urlSuffix
label: 网址后缀
placeholder: 例如:?imageMogr2/format/webp
- validation: required
\ No newline at end of file
+ validation: required
+ - $formkit: group
+ label: 缩略图参数
+ name: thumbnailParam
+ children:
+ - $formkit: select
+ name: type
+ key: type
+ label: 类型
+ options:
+ - label: 无
+ value: ""
+ - label: 预设参数
+ value: preset
+ - label: 自定义参数
+ value: custom
+ - $formkit: select
+ if: "$value.type == preset"
+ name: pattern
+ key: type
+ label: 使用预设参数
+ help: 请根据您的对象存储服务商选择对应的缩略图参数
+ options:
+ - label: 腾讯云 COS
+ value: "?imageView2/0/w/{width}"
+ - label: 七牛云 KODO
+ value: "?imageView2/0/w/{width}"
+ - label: 阿里云 OSS
+ value: "?x-oss-process=image/resize,w_{width},m_lfit"
+ - label: 百度云 BOS
+ value: "?x-bce-process=image/resize,m_lfit,w_{width}"
+ - label: 青云 OSS
+ value: "?image&action=resize:w_{width},m_2"
+ - label: 京东云
+ value: "?x-oss-process=img/sw/{width}"
+ - label: 又拍云
+ value: "!/fw/{width}"
+ - $formkit: text
+ if: "$value.type == custom"
+ label: 自定义参数
+ help: "{width} 为宽度占位符将被替换为所需缩略图宽度值,如: 400,参数需要以 ? 开头,间隔符除外"
+ name: pattern
diff --git a/src/test/java/run/halo/s3os/AttachmentThumbnailReconcilerTest.java b/src/test/java/run/halo/s3os/AttachmentThumbnailReconcilerTest.java
new file mode 100644
index 0000000..20d2ca3
--- /dev/null
+++ b/src/test/java/run/halo/s3os/AttachmentThumbnailReconcilerTest.java
@@ -0,0 +1,25 @@
+package run.halo.s3os;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.time.Duration;
+import java.time.Instant;
+import org.junit.jupiter.api.Test;
+
+class AttachmentThumbnailReconcilerTest {
+
+ @Test
+ void isMadeWithIn1DayTest() {
+ var madeTime = Instant.now().plusSeconds(2).toString();
+ var result = AttachmentThumbnailReconciler.isMadeWithIn1Day(madeTime);
+ assertThat(result).isTrue();
+
+ madeTime = Instant.now().plus(Duration.ofDays(2)).toString();
+ result = AttachmentThumbnailReconciler.isMadeWithIn1Day(madeTime);
+ assertThat(result).isTrue();
+
+ madeTime = Instant.now().minus(Duration.ofHours(25)).toString();
+ result = AttachmentThumbnailReconciler.isMadeWithIn1Day(madeTime);
+ assertThat(result).isFalse();
+ }
+}
From 1e44ad4713ebc68f8244e5517ed0b02fbef0029d Mon Sep 17 00:00:00 2001
From: guqing
Date: Wed, 4 Sep 2024 14:35:44 +0800
Subject: [PATCH 2/4] refactor: setting options definition for thumbnail param
pattern
---
build.gradle | 2 +-
.../s3os/AttachmentThumbnailReconciler.java | 74 -------------------
.../java/run/halo/s3os/S3OsProperties.java | 8 +-
.../run/halo/s3os/S3ThumbnailProvider.java | 12 +--
.../extensions/policy-template-s3os.yaml | 65 +++++++---------
.../AttachmentThumbnailReconcilerTest.java | 25 -------
6 files changed, 34 insertions(+), 152 deletions(-)
delete mode 100644 src/main/java/run/halo/s3os/AttachmentThumbnailReconciler.java
delete mode 100644 src/test/java/run/halo/s3os/AttachmentThumbnailReconcilerTest.java
diff --git a/build.gradle b/build.gradle
index af92100..755c372 100644
--- a/build.gradle
+++ b/build.gradle
@@ -40,7 +40,7 @@ configurations.runtimeClasspath {
halo {
- version = '2.17.0'
+ version = '2.19'
}
haloPlugin {
diff --git a/src/main/java/run/halo/s3os/AttachmentThumbnailReconciler.java b/src/main/java/run/halo/s3os/AttachmentThumbnailReconciler.java
deleted file mode 100644
index d662174..0000000
--- a/src/main/java/run/halo/s3os/AttachmentThumbnailReconciler.java
+++ /dev/null
@@ -1,74 +0,0 @@
-package run.halo.s3os;
-
-import static run.halo.app.infra.FileCategoryMatcher.IMAGE;
-
-import java.time.Duration;
-import java.time.Instant;
-import lombok.RequiredArgsConstructor;
-import org.apache.commons.lang3.StringUtils;
-import org.springframework.stereotype.Component;
-import org.springframework.util.Assert;
-import run.halo.app.core.extension.attachment.Attachment;
-import run.halo.app.extension.ExtensionClient;
-import run.halo.app.extension.MetadataUtil;
-import run.halo.app.extension.controller.Controller;
-import run.halo.app.extension.controller.ControllerBuilder;
-import run.halo.app.extension.controller.Reconciler;
-
-/**
- * This {@link Reconciler} used to check thumbnail status are generated
- * if not, update the attachment to trigger thumbnail generation by halo
- */
-@Component
-@RequiredArgsConstructor
-public class AttachmentThumbnailReconciler implements Reconciler {
- static final String REQUEST_GEN_THUMBNAIL = "s3os.halo.run/request-gen-thumbnail";
- private final ExtensionClient client;
-
- @Override
- public Result reconcile(Request request) {
- client.fetch(Attachment.class, request.name())
- .ifPresent(attachment -> {
- var annotations = MetadataUtil.nullSafeAnnotations(attachment);
- var requestTime = annotations.get(REQUEST_GEN_THUMBNAIL);
- if (isMadeWithIn1Day(requestTime)) {
- // skip if request is made within 1 day
- return;
- }
-
- if (!isImage(attachment)) {
- // skip non-image attachments
- return;
- }
-
- var status = attachment.getStatus();
- if (status == null || status.getThumbnails() == null) {
- // update to trigger attachment thumbnail generation
- annotations.put(REQUEST_GEN_THUMBNAIL, Instant.now().toString());
- client.update(attachment);
- }
- });
- return Result.doNotRetry();
- }
-
- static boolean isMadeWithIn1Day(String requestTimeStr) {
- if (StringUtils.isBlank(requestTimeStr)) {
- return false;
- }
- var requestTime = Instant.parse(requestTimeStr);
- return Duration.between(requestTime, Instant.now()).minusDays(1).isNegative();
- }
-
- public static boolean isImage(Attachment attachment) {
- Assert.notNull(attachment, "Attachment must not be null");
- var mediaType = attachment.getSpec().getMediaType();
- return mediaType != null && IMAGE.match(mediaType);
- }
-
- @Override
- public Controller setupWith(ControllerBuilder builder) {
- return builder
- .extension(new Attachment())
- .build();
- }
-}
diff --git a/src/main/java/run/halo/s3os/S3OsProperties.java b/src/main/java/run/halo/s3os/S3OsProperties.java
index b87c7da..3d10e4a 100644
--- a/src/main/java/run/halo/s3os/S3OsProperties.java
+++ b/src/main/java/run/halo/s3os/S3OsProperties.java
@@ -50,13 +50,7 @@ public class S3OsProperties {
private List urlSuffixes;
- private ThumbnailParam thumbnailParam;
-
- public record ThumbnailParam(String type, String pattern) {
- public boolean hasPattern() {
- return StringUtils.hasText(type) && StringUtils.hasText(pattern);
- }
- }
+ private String thumbnailParamPattern;
@Data
@AllArgsConstructor
diff --git a/src/main/java/run/halo/s3os/S3ThumbnailProvider.java b/src/main/java/run/halo/s3os/S3ThumbnailProvider.java
index 0e6ac8c..c0dec7c 100644
--- a/src/main/java/run/halo/s3os/S3ThumbnailProvider.java
+++ b/src/main/java/run/halo/s3os/S3ThumbnailProvider.java
@@ -43,8 +43,10 @@ public Mono generate(ThumbnailContext thumbnailContext) {
.mapNotNull(cacheValue -> placedPattern(cacheValue.pattern(), size))
.map(param -> {
if (param.startsWith("?")) {
- UriComponentsBuilder.fromHttpUrl(url)
- .queryParam(param.substring(1));
+ return UriComponentsBuilder.fromHttpUrl(url)
+ .queryParam(param.substring(1))
+ .build()
+ .toString();
}
return url + param;
})
@@ -91,13 +93,13 @@ private Flux> listAllS3ObjectDomain() {
var s3ConfigMapName = s3Policy.getSpec().getConfigMapName();
return fetchS3PropsByConfigMapName(s3ConfigMapName)
.mapNotNull(properties -> {
- var thumbnailParam = properties.getThumbnailParam();
- if (thumbnailParam == null || !thumbnailParam.hasPattern()) {
+ var thumbnailParam = properties.getThumbnailParamPattern();
+ if (StringUtils.isBlank(thumbnailParam)) {
return null;
}
var objectDomain = properties.toObjectURL("");
var cacheValue = S3PropsCacheValue.builder()
- .pattern(thumbnailParam.pattern())
+ .pattern(thumbnailParam)
.configMapName(s3ConfigMapName)
.build();
return Map.entry(objectDomain, cacheValue);
diff --git a/src/main/resources/extensions/policy-template-s3os.yaml b/src/main/resources/extensions/policy-template-s3os.yaml
index f44e4c5..437ed34 100644
--- a/src/main/resources/extensions/policy-template-s3os.yaml
+++ b/src/main/resources/extensions/policy-template-s3os.yaml
@@ -145,44 +145,29 @@ spec:
label: 网址后缀
placeholder: 例如:?imageMogr2/format/webp
validation: required
- - $formkit: group
+ - $formkit: select
+ name: thumbnailParamPattern
label: 缩略图参数
- name: thumbnailParam
- children:
- - $formkit: select
- name: type
- key: type
- label: 类型
- options:
- - label: 无
- value: ""
- - label: 预设参数
- value: preset
- - label: 自定义参数
- value: custom
- - $formkit: select
- if: "$value.type == preset"
- name: pattern
- key: type
- label: 使用预设参数
- help: 请根据您的对象存储服务商选择对应的缩略图参数
- options:
- - label: 腾讯云 COS
- value: "?imageView2/0/w/{width}"
- - label: 七牛云 KODO
- value: "?imageView2/0/w/{width}"
- - label: 阿里云 OSS
- value: "?x-oss-process=image/resize,w_{width},m_lfit"
- - label: 百度云 BOS
- value: "?x-bce-process=image/resize,m_lfit,w_{width}"
- - label: 青云 OSS
- value: "?image&action=resize:w_{width},m_2"
- - label: 京东云
- value: "?x-oss-process=img/sw/{width}"
- - label: 又拍云
- value: "!/fw/{width}"
- - $formkit: text
- if: "$value.type == custom"
- label: 自定义参数
- help: "{width} 为宽度占位符将被替换为所需缩略图宽度值,如: 400,参数需要以 ? 开头,间隔符除外"
- name: pattern
+ allowCreate: true
+ searchable: true
+ value: ""
+ help: |
+ 请根据您的对象存储服务商选择对应的缩略图参数或自定义参数,{width} 为宽度占位符将被替换为所需缩略图宽度值,
+ 如: 400,参数需要以 ? 开头,间隔符除外
+ options:
+ - label: 无
+ value: ""
+ - label: 腾讯云 COS
+ value: "?imageView2/0/w/{width}"
+ - label: 七牛云 KODO
+ value: "?imageView2/0/w/{width}"
+ - label: 阿里云 OSS
+ value: "?x-oss-process=image/resize,w_{width},m_lfit"
+ - label: 百度云 BOS
+ value: "?x-bce-process=image/resize,m_lfit,w_{width}"
+ - label: 青云 OSS
+ value: "?image&action=resize:w_{width},m_2"
+ - label: 京东云
+ value: "?x-oss-process=img/sw/{width}"
+ - label: 又拍云
+ value: "!/fw/{width}"
diff --git a/src/test/java/run/halo/s3os/AttachmentThumbnailReconcilerTest.java b/src/test/java/run/halo/s3os/AttachmentThumbnailReconcilerTest.java
deleted file mode 100644
index 20d2ca3..0000000
--- a/src/test/java/run/halo/s3os/AttachmentThumbnailReconcilerTest.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package run.halo.s3os;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-import java.time.Duration;
-import java.time.Instant;
-import org.junit.jupiter.api.Test;
-
-class AttachmentThumbnailReconcilerTest {
-
- @Test
- void isMadeWithIn1DayTest() {
- var madeTime = Instant.now().plusSeconds(2).toString();
- var result = AttachmentThumbnailReconciler.isMadeWithIn1Day(madeTime);
- assertThat(result).isTrue();
-
- madeTime = Instant.now().plus(Duration.ofDays(2)).toString();
- result = AttachmentThumbnailReconciler.isMadeWithIn1Day(madeTime);
- assertThat(result).isTrue();
-
- madeTime = Instant.now().minus(Duration.ofHours(25)).toString();
- result = AttachmentThumbnailReconciler.isMadeWithIn1Day(madeTime);
- assertThat(result).isFalse();
- }
-}
From a98a4627ca098f553505b303e6bcc39b3dd23eb0 Mon Sep 17 00:00:00 2001
From: guqing
Date: Thu, 19 Sep 2024 11:00:40 +0800
Subject: [PATCH 3/4] chore: update plugin requires
---
src/main/resources/plugin.yaml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/main/resources/plugin.yaml b/src/main/resources/plugin.yaml
index 87762cd..fd2bda7 100644
--- a/src/main/resources/plugin.yaml
+++ b/src/main/resources/plugin.yaml
@@ -4,7 +4,7 @@ metadata:
name: PluginS3ObjectStorage
spec:
enabled: true
- requires: ">=2.17.0"
+ requires: ">=2.19.0"
author:
name: Halo
website: https://github.com/halo-dev
From b90c331c08b03a0c9a852f03950f5d373b3a2c5e Mon Sep 17 00:00:00 2001
From: guqing
Date: Mon, 23 Sep 2024 11:52:18 +0800
Subject: [PATCH 4/4] fix: code style
---
src/main/java/run/halo/s3os/S3OsPlugin.java | 1 -
.../run/halo/s3os/S3ThumbnailProvider.java | 28 ++-----------------
.../extensions/policy-template-s3os.yaml | 4 +--
3 files changed, 3 insertions(+), 30 deletions(-)
diff --git a/src/main/java/run/halo/s3os/S3OsPlugin.java b/src/main/java/run/halo/s3os/S3OsPlugin.java
index 5c3cdbd..7851027 100644
--- a/src/main/java/run/halo/s3os/S3OsPlugin.java
+++ b/src/main/java/run/halo/s3os/S3OsPlugin.java
@@ -10,7 +10,6 @@
*/
@Component
public class S3OsPlugin extends BasePlugin {
- public static final String POLICY_SETTING_NAME = "s3os-policy-template-setting";
public S3OsPlugin(PluginContext pluginContext) {
super(pluginContext);
diff --git a/src/main/java/run/halo/s3os/S3ThumbnailProvider.java b/src/main/java/run/halo/s3os/S3ThumbnailProvider.java
index c0dec7c..f7cc36e 100644
--- a/src/main/java/run/halo/s3os/S3ThumbnailProvider.java
+++ b/src/main/java/run/halo/s3os/S3ThumbnailProvider.java
@@ -1,28 +1,20 @@
package run.halo.s3os;
-import static run.halo.s3os.S3OsPlugin.POLICY_SETTING_NAME;
-
import java.net.URI;
import java.net.URL;
-import java.util.List;
import java.util.Map;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import lombok.Builder;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
-import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Component;
-import org.springframework.util.Assert;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import run.halo.app.core.attachment.ThumbnailProvider;
import run.halo.app.core.attachment.ThumbnailSize;
-import run.halo.app.core.extension.attachment.Policy;
-import run.halo.app.core.extension.attachment.PolicyTemplate;
import run.halo.app.extension.ConfigMap;
-import run.halo.app.extension.ListOptions;
import run.halo.app.extension.ReactiveExtensionClient;
@Component
@@ -34,6 +26,7 @@ public class S3ThumbnailProvider implements ThumbnailProvider {
.build();
private final ReactiveExtensionClient client;
+ private final S3LinkService s3LinkService;
@Override
public Mono generate(ThumbnailContext thumbnailContext) {
@@ -86,9 +79,7 @@ record S3PropsCacheValue(String pattern, String configMapName) {
}
private Flux> listAllS3ObjectDomain() {
- return listS3PolicyTemplateNames()
- .collectList()
- .flatMapMany(this::listAllS3Policy)
+ return s3LinkService.listS3Policies()
.flatMap(s3Policy -> {
var s3ConfigMapName = s3Policy.getSpec().getConfigMapName();
return fetchS3PropsByConfigMapName(s3ConfigMapName)
@@ -108,23 +99,8 @@ private Flux> listAllS3ObjectDomain() {
.doOnNext(cache -> s3PropsCache.put(cache.getKey(), cache.getValue()));
}
- private Flux listAllS3Policy(List s3PolicyTemplateNames) {
- Assert.notNull(s3PolicyTemplateNames, "The s3PolicyTemplateNames must not be null.");
- return client.listAll(Policy.class, new ListOptions(), Sort.unsorted())
- .filter(policy -> {
- var templateName = policy.getSpec().getTemplateName();
- return s3PolicyTemplateNames.contains(templateName);
- });
- }
-
private Mono fetchS3PropsByConfigMapName(String name) {
return client.fetch(ConfigMap.class, name)
.map(S3OsProperties::convertFrom);
}
-
- private Flux listS3PolicyTemplateNames() {
- return client.listAll(PolicyTemplate.class, new ListOptions(), Sort.unsorted())
- .filter(policyTemplate -> POLICY_SETTING_NAME.equals(policyTemplate.getSpec().getSettingName()))
- .map(template -> template.getMetadata().getName());
- }
}
diff --git a/src/main/resources/extensions/policy-template-s3os.yaml b/src/main/resources/extensions/policy-template-s3os.yaml
index 437ed34..ecb61b4 100644
--- a/src/main/resources/extensions/policy-template-s3os.yaml
+++ b/src/main/resources/extensions/policy-template-s3os.yaml
@@ -157,9 +157,7 @@ spec:
options:
- label: 无
value: ""
- - label: 腾讯云 COS
- value: "?imageView2/0/w/{width}"
- - label: 七牛云 KODO
+ - label: 腾讯云 COS / 七牛云 KODO
value: "?imageView2/0/w/{width}"
- label: 阿里云 OSS
value: "?x-oss-process=image/resize,w_{width},m_lfit"