mirror of
https://github.com/halo-dev/plugin-s3.git
synced 2026-01-14 07:03:32 +08:00
Update S3 plugin version and enhance thumbnail link generation
This commit is contained in:
@@ -25,7 +25,7 @@ repositories {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation platform('run.halo.tools.platform:plugin:2.21.0-alpha.1')
|
||||
implementation platform('run.halo.tools.platform:plugin:2.22.0-alpha.1')
|
||||
compileOnly 'run.halo.app:api'
|
||||
|
||||
implementation platform('software.amazon.awssdk:bom:2.31.58')
|
||||
|
||||
@@ -1,15 +1,5 @@
|
||||
package run.halo.s3os;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.file.FileAlreadyExistsException;
|
||||
import java.time.Duration;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.pf4j.Extension;
|
||||
@@ -19,17 +9,21 @@ import org.springframework.core.io.buffer.DataBufferUtils;
|
||||
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.MediaTypeFactory;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.web.server.ServerErrorException;
|
||||
import org.springframework.web.server.ServerWebInputException;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
import reactor.core.Exceptions;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
import reactor.util.context.Context;
|
||||
import reactor.util.retry.Retry;
|
||||
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.Attachment.AttachmentStatus;
|
||||
import run.halo.app.core.extension.attachment.Constant;
|
||||
import run.halo.app.core.extension.attachment.Policy;
|
||||
import run.halo.app.core.extension.attachment.endpoint.AttachmentHandler;
|
||||
@@ -44,18 +38,47 @@ import software.amazon.awssdk.http.SdkHttpResponse;
|
||||
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.*;
|
||||
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.presigner.S3Presigner;
|
||||
import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;
|
||||
import software.amazon.awssdk.utils.SdkAutoCloseable;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.file.FileAlreadyExistsException;
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@Extension
|
||||
public class S3OsAttachmentHandler implements AttachmentHandler {
|
||||
|
||||
public static final String OBJECT_KEY = "s3os.plugin.halo.run/object-key";
|
||||
|
||||
public static final String URL_SUFFIX_ANNO_KEY = "s3os.plugin.halo.run/url-suffix";
|
||||
|
||||
public static final String SKIP_REMOTE_DELETION_ANNO = "s3os.plugin.halo.run/skip-remote-deletion";
|
||||
|
||||
private static final MediaType IMAGE_MEDIA_TYPE = MediaType.parseMediaType("image/*");
|
||||
|
||||
public static final int MULTIPART_MIN_PART_SIZE = 5 * 1024 * 1024;
|
||||
|
||||
/**
|
||||
@@ -156,18 +179,73 @@ public class S3OsAttachmentHandler implements AttachmentHandler {
|
||||
if (!this.shouldHandle(policy)) {
|
||||
return Mono.empty();
|
||||
}
|
||||
var objectKey = getObjectKey(attachment);
|
||||
if (objectKey == null) {
|
||||
// fallback to default handler for backward compatibility
|
||||
return Mono.justOrEmpty(doGetPermalink(attachment, S3OsProperties.convertFrom(configMap)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Map<ThumbnailSize, URI>> getThumbnailLinks(Attachment attachment, Policy policy, ConfigMap configMap) {
|
||||
if (!this.shouldHandle(policy)) {
|
||||
return Mono.empty();
|
||||
}
|
||||
var properties = S3OsProperties.convertFrom(configMap);
|
||||
return Mono.just(doGetThumbnailLinks(attachment, properties));
|
||||
}
|
||||
|
||||
private Optional<URI> doGetPermalink(Attachment attachment, S3OsProperties properties) {
|
||||
var objectKey = getObjectKey(attachment);
|
||||
if (objectKey == null) {
|
||||
// fallback to default handler for backward compatibility
|
||||
return Optional.empty();
|
||||
}
|
||||
var objectURL = properties.toObjectURL(objectKey);
|
||||
var urlSuffix = getUrlSuffixAnnotation(attachment);
|
||||
if (StringUtils.isNotBlank(urlSuffix)) {
|
||||
objectURL += urlSuffix;
|
||||
}
|
||||
return Mono.just(URI.create(objectURL));
|
||||
return Optional.of(URI.create(objectURL));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private Map<ThumbnailSize, URI> doGetThumbnailLinks(Attachment attachment, S3OsProperties properties) {
|
||||
// TODO Support configuring media types that support thumbnails
|
||||
var support = Optional.ofNullable(attachment.getSpec().getMediaType())
|
||||
.map(MediaType::parseMediaType)
|
||||
.map(IMAGE_MEDIA_TYPE::isCompatibleWith)
|
||||
.orElse(false);
|
||||
if (!support) {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Attachment {} media type {} is not compatible with image/*, skip generating thumbnail links",
|
||||
attachment.getMetadata().getName(), attachment.getSpec().getMediaType());
|
||||
}
|
||||
return Map.of();
|
||||
}
|
||||
|
||||
var thumbnailParamPattern = properties.getThumbnailParamPattern();
|
||||
if (StringUtils.isBlank(thumbnailParamPattern) || !thumbnailParamPattern.contains("{width}")) {
|
||||
return Map.of();
|
||||
}
|
||||
return Optional.ofNullable(attachment.getStatus())
|
||||
.map(AttachmentStatus::getPermalink)
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.map(URI::create)
|
||||
.or(() -> doGetPermalink(attachment, properties))
|
||||
.map(permalink -> Arrays.stream(ThumbnailSize.values())
|
||||
.collect(Collectors.toMap(Function.identity(), size -> {
|
||||
var thumbnailParam = thumbnailParamPattern.replace("{width}", String.valueOf(size.getWidth()));
|
||||
var isQueryPattern = thumbnailParam.startsWith("?");
|
||||
if (isQueryPattern) {
|
||||
return UriComponentsBuilder.fromUri(permalink)
|
||||
.query(thumbnailParam.substring(1))
|
||||
.build(true)
|
||||
.toUri();
|
||||
}
|
||||
return UriComponentsBuilder.fromUri(permalink)
|
||||
.path(thumbnailParam)
|
||||
.build(true)
|
||||
.toUri();
|
||||
}))
|
||||
)
|
||||
.orElse(Map.of());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@@ -213,6 +291,17 @@ public class S3OsAttachmentHandler implements AttachmentHandler {
|
||||
var attachment = new Attachment();
|
||||
attachment.setMetadata(metadata);
|
||||
attachment.setSpec(spec);
|
||||
attachment.setStatus(new AttachmentStatus());
|
||||
doGetPermalink(attachment, properties).ifPresent(permalink ->
|
||||
attachment.getStatus().setPermalink(permalink.toString())
|
||||
);
|
||||
var thumbnails = doGetThumbnailLinks(attachment, properties);
|
||||
var mappedThumbnails = thumbnails.keySet()
|
||||
.stream()
|
||||
.collect(Collectors.toMap(ThumbnailSize::name, size -> thumbnails.get(size).toString()));
|
||||
if (!mappedThumbnails.isEmpty()) {
|
||||
attachment.getStatus().setThumbnails(mappedThumbnails);
|
||||
}
|
||||
log.info("Built attachment {} successfully", objectDetail.uploadState.objectKey);
|
||||
return attachment;
|
||||
}
|
||||
@@ -369,8 +458,8 @@ public class S3OsAttachmentHandler implements AttachmentHandler {
|
||||
if (uploadingFile.put(uploadState.getUploadingMapKey(),
|
||||
uploadState.getUploadingMapKey()) != null) {
|
||||
return Mono.error(new FileAlreadyExistsException("文件 " + uploadState.objectKey
|
||||
+
|
||||
" 已存在,建议更名后重试。[local]"));
|
||||
+
|
||||
" 已存在,建议更名后重试。[local]"));
|
||||
}
|
||||
uploadState.needRemoveMapKey = true;
|
||||
// check whether file exists
|
||||
@@ -388,7 +477,7 @@ public class S3OsAttachmentHandler implements AttachmentHandler {
|
||||
&& response.sdkHttpResponse().isSuccessful()) {
|
||||
return Mono.error(
|
||||
new FileAlreadyExistsException("文件 " + uploadState.objectKey
|
||||
+ " 已存在,建议更名后重试。[remote]"));
|
||||
+ " 已存在,建议更名后重试。[remote]"));
|
||||
} else {
|
||||
return Mono.just(uploadState);
|
||||
}
|
||||
@@ -463,18 +552,29 @@ public class S3OsAttachmentHandler implements AttachmentHandler {
|
||||
}
|
||||
|
||||
record ObjectDetail(UploadState uploadState, HeadObjectResponse objectMetadata) {
|
||||
|
||||
}
|
||||
|
||||
static class UploadState {
|
||||
|
||||
final S3OsProperties properties;
|
||||
|
||||
final String originalFileName;
|
||||
|
||||
String uploadId;
|
||||
|
||||
int partCounter;
|
||||
|
||||
Map<Integer, CompletedPart> completedParts = new HashMap<>();
|
||||
|
||||
int buffered = 0;
|
||||
|
||||
String contentType;
|
||||
|
||||
String fileName;
|
||||
|
||||
String objectKey;
|
||||
|
||||
boolean needRemoveMapKey = false;
|
||||
|
||||
public UploadState(S3OsProperties properties, String fileName, boolean needRandomJudge) {
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
package run.halo.s3os;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
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.stereotype.Component;
|
||||
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.extension.ConfigMap;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class S3ThumbnailProvider implements ThumbnailProvider {
|
||||
static final String WIDTH_PLACEHOLDER = "{width}";
|
||||
private final Cache<String, S3PropsCacheValue> s3PropsCache = CacheBuilder.newBuilder()
|
||||
.maximumSize(50)
|
||||
.build();
|
||||
|
||||
private final ReactiveExtensionClient client;
|
||||
private final S3LinkService s3LinkService;
|
||||
|
||||
@Override
|
||||
public Mono<URI> 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("?")) {
|
||||
return UriComponentsBuilder.fromHttpUrl(url)
|
||||
.queryParam(param.substring(1))
|
||||
.build()
|
||||
.toString();
|
||||
}
|
||||
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<Void> delete(URL url) {
|
||||
// do nothing for s3
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Boolean> supports(ThumbnailContext thumbnailContext) {
|
||||
var url = thumbnailContext.getImageUrl().toString();
|
||||
return getCacheValue(url).hasElement();
|
||||
}
|
||||
|
||||
private Mono<S3PropsCacheValue> 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<Map.Entry<String, S3PropsCacheValue>> listAllS3ObjectDomain() {
|
||||
return s3LinkService.listS3Policies()
|
||||
.flatMap(s3Policy -> {
|
||||
var s3ConfigMapName = s3Policy.getSpec().getConfigMapName();
|
||||
return fetchS3PropsByConfigMapName(s3ConfigMapName)
|
||||
.mapNotNull(properties -> {
|
||||
var thumbnailParam = properties.getThumbnailParamPattern();
|
||||
if (StringUtils.isBlank(thumbnailParam)) {
|
||||
return null;
|
||||
}
|
||||
var objectDomain = properties.toObjectURL("");
|
||||
var cacheValue = S3PropsCacheValue.builder()
|
||||
.pattern(thumbnailParam)
|
||||
.configMapName(s3ConfigMapName)
|
||||
.build();
|
||||
return Map.entry(objectDomain, cacheValue);
|
||||
});
|
||||
})
|
||||
.doOnNext(cache -> s3PropsCache.put(cache.getKey(), cache.getValue()));
|
||||
}
|
||||
|
||||
private Mono<S3OsProperties> fetchS3PropsByConfigMapName(String name) {
|
||||
return client.fetch(ConfigMap.class, name)
|
||||
.map(S3OsProperties::convertFrom);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ metadata:
|
||||
name: PluginS3ObjectStorage
|
||||
spec:
|
||||
enabled: true
|
||||
requires: ">=2.21.0"
|
||||
requires: ">=2.22.0"
|
||||
author:
|
||||
name: Halo
|
||||
website: https://github.com/halo-dev
|
||||
|
||||
@@ -1,5 +1,22 @@
|
||||
package run.halo.s3os;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.test.StepVerifier;
|
||||
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.Metadata;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
@@ -7,15 +24,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.test.StepVerifier;
|
||||
import run.halo.app.core.extension.attachment.Policy;
|
||||
|
||||
class S3OsAttachmentHandlerTest {
|
||||
|
||||
S3OsAttachmentHandler handler;
|
||||
@@ -93,4 +101,112 @@ class S3OsAttachmentHandlerTest {
|
||||
})
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldGetThumbnailsIfPatternIsQueryParam() {
|
||||
var attachment = createAttachment("https://s3.halo.run/halo.png?existing-query=existing-value");
|
||||
|
||||
var policy = new Policy();
|
||||
policy.setSpec(new Policy.PolicySpec());
|
||||
policy.getSpec().setTemplateName("s3os");
|
||||
|
||||
var configMap = new ConfigMap();
|
||||
configMap.setData(new HashMap<>());
|
||||
configMap.getData().put("default", """
|
||||
{
|
||||
"thumbnailParamPattern": "?width={width}&quality=80"
|
||||
}
|
||||
""");
|
||||
handler.getThumbnailLinks(attachment, policy, configMap)
|
||||
.as(StepVerifier::create)
|
||||
.expectNext(Map.of(
|
||||
ThumbnailSize.S, URI.create("https://s3.halo.run/halo.png?existing-query=existing-value&width=400&quality=80"),
|
||||
ThumbnailSize.M, URI.create("https://s3.halo.run/halo.png?existing-query=existing-value&width=800&quality=80"),
|
||||
ThumbnailSize.L, URI.create("https://s3.halo.run/halo.png?existing-query=existing-value&width=1200&quality=80"),
|
||||
ThumbnailSize.XL, URI.create("https://s3.halo.run/halo.png?existing-query=existing-value&width=1600&quality=80")
|
||||
))
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldGetThumbnailsIfPatternIsPath() {
|
||||
var attachment = createAttachment("https://s3.halo.run/existing-path/halo.png");
|
||||
|
||||
var policy = new Policy();
|
||||
policy.setSpec(new Policy.PolicySpec());
|
||||
policy.getSpec().setTemplateName("s3os");
|
||||
|
||||
var configMap = new ConfigMap();
|
||||
configMap.setData(new HashMap<>());
|
||||
configMap.getData().put("default", """
|
||||
{
|
||||
"thumbnailParamPattern": "!path/width/{width}"
|
||||
}
|
||||
""");
|
||||
handler.getThumbnailLinks(attachment, policy, configMap)
|
||||
.as(StepVerifier::create)
|
||||
.expectNext(Map.of(
|
||||
ThumbnailSize.S, URI.create("https://s3.halo.run/existing-path/halo.png!path/width/400"),
|
||||
ThumbnailSize.M, URI.create("https://s3.halo.run/existing-path/halo.png!path/width/800"),
|
||||
ThumbnailSize.L, URI.create("https://s3.halo.run/existing-path/halo.png!path/width/1200"),
|
||||
ThumbnailSize.XL, URI.create("https://s3.halo.run/existing-path/halo.png!path/width/1600")
|
||||
))
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldGetEmptyThumbnailsIfNoPattern() {
|
||||
var attachment = createAttachment("https://s3.halo.run/halo.png");
|
||||
|
||||
var policy = new Policy();
|
||||
policy.setSpec(new Policy.PolicySpec());
|
||||
policy.getSpec().setTemplateName("s3os");
|
||||
|
||||
var configMap = new ConfigMap();
|
||||
configMap.setData(new HashMap<>());
|
||||
configMap.getData().put("default", """
|
||||
{}
|
||||
""");
|
||||
handler.getThumbnailLinks(attachment, policy, configMap)
|
||||
.as(StepVerifier::create)
|
||||
.expectNext(Map.of())
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldGetEmptyThumbnailsIfNotImage() {
|
||||
var attachment = createAttachment("application/pdf", "https://s3.halo.run/halo.pdf");
|
||||
|
||||
var policy = new Policy();
|
||||
policy.setSpec(new Policy.PolicySpec());
|
||||
policy.getSpec().setTemplateName("s3os");
|
||||
|
||||
var configMap = new ConfigMap();
|
||||
configMap.setData(new HashMap<>());
|
||||
configMap.getData().put("default", """
|
||||
{
|
||||
"thumbnailParamPattern": "!path/width/{width}"
|
||||
}
|
||||
""");
|
||||
handler.getThumbnailLinks(attachment, policy, configMap)
|
||||
.as(StepVerifier::create)
|
||||
.expectNext(Map.of())
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
static Attachment createAttachment(String permalink) {
|
||||
return createAttachment("image/png", permalink);
|
||||
}
|
||||
|
||||
static Attachment createAttachment(String mediaType, String permalink) {
|
||||
var attachment = new Attachment();
|
||||
attachment.setMetadata(new Metadata());
|
||||
attachment.getMetadata().setName("fake-attachment");
|
||||
attachment.setSpec(new Attachment.AttachmentSpec());
|
||||
attachment.getSpec().setMediaType(mediaType);
|
||||
attachment.setStatus(new Attachment.AttachmentStatus());
|
||||
attachment.getStatus().setPermalink(permalink);
|
||||
return attachment;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user