Support to get shared URL and permalink of attachment in handler (#35)

On the Halo side, PR https://github.com/halo-dev/halo/pull/3740 has already added two new methods (`getSharedURL` and `getPermalink`) into AttachmentHandler. Now It's time to implement these two methods so that users can correctly and easily use these two methods.

This PR mainly implements [new AttachmentHandler](11a5807682/api/src/main/java/run/halo/app/core/extension/attachment/endpoint/AttachmentHandler.java). At the same time, I also refactored the build script for a better development experience.

Please note that, those changes might not influence compatibility with Halo 2.0.0. You can have test against Halo 2.0.0 manually.

/kind feature

```release-note
支持获取分享链接和永久链接
```
This commit is contained in:
John Niang
2023-04-21 20:33:40 +08:00
committed by GitHub
parent 5e9b9f803b
commit 88490bb80f
6 changed files with 103 additions and 20 deletions

View File

@@ -1,5 +1,6 @@
plugins {
id "io.github.guqing.plugin-development" version "0.0.6-SNAPSHOT"
id "io.github.guqing.plugin-development" version "0.0.7-SNAPSHOT"
id "io.freefair.lombok" version "8.0.0-rc2"
id 'java'
}
@@ -8,7 +9,7 @@ sourceCompatibility = JavaVersion.VERSION_17
repositories {
maven { url 'https://s01.oss.sonatype.org/content/repositories/releases' }
maven { url 'https://repo.spring.io/milestone' }
maven { url 'https://s01.oss.sonatype.org/content/repositories/snapshots/' }
mavenCentral()
}
@@ -23,9 +24,8 @@ jar {
}
dependencies {
compileOnly platform("run.halo.dependencies:halo-dependencies:1.0.0")
compileOnly files("lib/halo-2.0.0-SNAPSHOT-plain.jar")
implementation platform('run.halo.tools.platform:plugin:2.5.0-SNAPSHOT')
compileOnly 'run.halo.app:api'
implementation platform('software.amazon.awssdk:bom:2.19.8')
implementation 'software.amazon.awssdk:s3'
@@ -33,14 +33,8 @@ dependencies {
implementation "javax.activation:activation:1.1.1"
implementation "org.glassfish.jaxb:jaxb-runtime:2.3.3"
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok:1.18.22'
testImplementation platform("run.halo.dependencies:halo-dependencies:1.0.0")
testImplementation files("lib/halo-2.0.0-SNAPSHOT-plain.jar")
testImplementation 'run.halo.app:api'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.0'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.0'
}
test {

View File

@@ -1 +1 @@
version=1.3.0-SNAPSHOT
version=1.4.0-SNAPSHOT

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

Binary file not shown.

View File

@@ -1,9 +1,11 @@
package run.halo.s3os;
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;
import java.util.List;
import java.util.Map;
@@ -15,6 +17,7 @@ import org.pf4j.Extension;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.MediaType;
import org.springframework.http.MediaTypeFactory;
import org.springframework.lang.Nullable;
import org.springframework.web.server.ServerErrorException;
import org.springframework.web.server.ServerWebInputException;
import org.springframework.web.util.UriUtils;
@@ -31,6 +34,7 @@ import run.halo.app.extension.ConfigMap;
import run.halo.app.extension.Metadata;
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;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.http.SdkHttpResponse;
@@ -42,10 +46,13 @@ 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;
@Slf4j
@@ -71,29 +78,88 @@ public class S3OsAttachmentHandler implements AttachmentHandler {
public Mono<Attachment> delete(DeleteContext deleteContext) {
return Mono.just(deleteContext).filter(context -> this.shouldHandle(context.policy()))
.flatMap(context -> {
var annotations = context.attachment().getMetadata().getAnnotations();
if (annotations == null || !annotations.containsKey(OBJECT_KEY)) {
var objectKey = getObjectKey(context.attachment());
if (objectKey == null) {
return Mono.just(context);
}
var objectName = annotations.get(OBJECT_KEY);
var properties = getProperties(deleteContext.configMap());
return Mono.using(() -> buildS3Client(properties),
client -> Mono.fromCallable(
() -> client.deleteObject(DeleteObjectRequest.builder()
.bucket(properties.getBucket())
.key(objectName)
.key(objectKey)
.build())).subscribeOn(Schedulers.boundedElastic()),
S3Client::close)
.doOnNext(response -> {
checkResult(response, "delete object");
log.info("Delete object {} from bucket {} successfully",
objectName, properties.getBucket());
objectKey, properties.getBucket());
})
.thenReturn(context);
})
.map(DeleteContext::attachment);
}
@Override
public Mono<URI> getSharedURL(Attachment attachment, Policy policy, ConfigMap configMap,
Duration ttl) {
if (!this.shouldHandle(policy)) {
return Mono.empty();
}
var objectKey = getObjectKey(attachment);
if (objectKey == null) {
return Mono.error(new IllegalArgumentException(
"Cannot obtain object key from attachment " + attachment.getMetadata().getName()));
}
var properties = getProperties(configMap);
return Mono.using(() -> buildS3Presigner(properties),
s3Presigner -> {
var getObjectRequest = GetObjectRequest.builder()
.bucket(properties.getBucket())
.key(objectKey)
.build();
var presignedRequest = GetObjectPresignRequest.builder()
.signatureDuration(Duration.ofMinutes(5))
.getObjectRequest(getObjectRequest)
.build();
var presignedGetObjectRequest = s3Presigner.presignGetObject(presignedRequest);
var presignedURL = presignedGetObjectRequest.url();
try {
return Mono.just(presignedURL.toURI());
} catch (URISyntaxException e) {
return Mono.error(
new RuntimeException("Failed to convert URL " + presignedURL + " to URI."));
}
},
SdkPresigner::close)
.subscribeOn(Schedulers.boundedElastic());
}
@Override
public Mono<URI> getPermalink(Attachment attachment, Policy policy, ConfigMap configMap) {
if (!this.shouldHandle(policy)) {
return Mono.empty();
}
var objectKey = getObjectKey(attachment);
if (objectKey == null) {
return Mono.error(new IllegalArgumentException(
"Cannot obtain object key from attachment " + attachment.getMetadata().getName()));
}
var properties = getProperties(configMap);
var objectName = getObjectName(properties, objectKey);
return Mono.just(URI.create(objectName));
}
@Nullable
private String getObjectKey(Attachment attachment) {
var annotations = attachment.getMetadata().getAnnotations();
if (annotations == null) {
return null;
}
return annotations.get(OBJECT_KEY);
}
S3OsProperties getProperties(ConfigMap configMap) {
var settingJson = configMap.getData().getOrDefault("default", "{}");
return JsonUtils.jsonToObject(settingJson, S3OsProperties.class);
@@ -130,6 +196,15 @@ public class S3OsAttachmentHandler implements AttachmentHandler {
return attachment;
}
private String getObjectName(S3OsProperties properties, String objectKey) {
if (StringUtils.isBlank(properties.getDomain())) {
var host = properties.getBucket() + "." + properties.getEndpoint();
return properties.getProtocol() + "://" + host + "/" + objectKey;
} else {
return properties.getProtocol() + "://" + properties.getDomain() + "/" + objectKey;
}
}
S3Client buildS3Client(S3OsProperties properties) {
return S3Client.builder()
.region(Region.of(properties.getRegion()))
@@ -144,6 +219,20 @@ public class S3OsAttachmentHandler implements AttachmentHandler {
.build();
}
private S3Presigner buildS3Presigner(S3OsProperties properties) {
return S3Presigner.builder()
.region(Region.of(properties.getRegion()))
.endpointOverride(
URI.create(properties.getEndpointProtocol() + "://" + properties.getEndpoint()))
.credentialsProvider(() -> AwsBasicCredentials.create(properties.getAccessKey(),
properties.getAccessSecret()))
.serviceConfiguration(S3Configuration.builder()
.chunkedEncodingEnabled(false)
.pathStyleAccessEnabled(properties.getEnablePathStyleAccess())
.build())
.build();
}
Mono<ObjectDetail> upload(UploadContext uploadContext, S3OsProperties properties) {
return Mono.using(() -> buildS3Client(properties),
client -> {

View File

@@ -4,7 +4,7 @@ metadata:
name: PluginS3ObjectStorage
spec:
enabled: true
version: 1.3.0
version: 1.4.0
requires: ">=2.0.0"
author:
name: longjuan