diff --git a/build.gradle b/build.gradle index a26e643..4c980b3 100644 --- a/build.gradle +++ b/build.gradle @@ -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 { diff --git a/gradle.properties b/gradle.properties index fe820f8..f271e55 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=1.3.0-SNAPSHOT +version=1.4.0-SNAPSHOT diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 41dfb87..e1bef7e 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/lib/halo-2.0.0-SNAPSHOT-plain.jar b/lib/halo-2.0.0-SNAPSHOT-plain.jar deleted file mode 100644 index 64cabb4..0000000 Binary files a/lib/halo-2.0.0-SNAPSHOT-plain.jar and /dev/null differ diff --git a/src/main/java/run/halo/s3os/S3OsAttachmentHandler.java b/src/main/java/run/halo/s3os/S3OsAttachmentHandler.java index 9880cb7..91f6cc8 100644 --- a/src/main/java/run/halo/s3os/S3OsAttachmentHandler.java +++ b/src/main/java/run/halo/s3os/S3OsAttachmentHandler.java @@ -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 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 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 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 upload(UploadContext uploadContext, S3OsProperties properties) { return Mono.using(() -> buildS3Client(properties), client -> { diff --git a/src/main/resources/plugin.yaml b/src/main/resources/plugin.yaml index a68573a..ebff42f 100644 --- a/src/main/resources/plugin.yaml +++ b/src/main/resources/plugin.yaml @@ -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