mirror of
https://github.com/halo-dev/plugin-s3.git
synced 2025-10-16 07:19:45 +00:00
Compare commits
9 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
6b62ce7aa4 | ||
![]() |
c60e31a033 | ||
![]() |
7a9b0de0c6 | ||
![]() |
5e7e6620fd | ||
![]() |
47b6a37d0a | ||
![]() |
68b1a88b14 | ||
![]() |
f4ec56b7bc | ||
![]() |
c5d4e719a7 | ||
![]() |
034b3f3ded |
17
.github/workflows/cd.yaml
vendored
Normal file
17
.github/workflows/cd.yaml
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
name: CD
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types:
|
||||||
|
- published
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
cd:
|
||||||
|
uses: halo-sigs/reusable-workflows/.github/workflows/plugin-cd.yaml@v1
|
||||||
|
secrets:
|
||||||
|
halo-username: ${{ secrets.HALO_USERNAME }}
|
||||||
|
halo-password: ${{ secrets.HALO_PASSWORD }}
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
with:
|
||||||
|
app-id: app-Qxhpp
|
13
.github/workflows/ci.yaml
vendored
Normal file
13
.github/workflows/ci.yaml
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
ci:
|
||||||
|
uses: halo-sigs/reusable-workflows/.github/workflows/plugin-ci.yaml@v1
|
138
.github/workflows/workflow.yaml
vendored
138
.github/workflows/workflow.yaml
vendored
@@ -1,138 +0,0 @@
|
|||||||
name: Build Plugin JAR File
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths:
|
|
||||||
- "**"
|
|
||||||
- "!**.md"
|
|
||||||
release:
|
|
||||||
types:
|
|
||||||
- published
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths:
|
|
||||||
- "**"
|
|
||||||
- "!**.md"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
with:
|
|
||||||
submodules: true
|
|
||||||
- name: Set up JDK 17
|
|
||||||
uses: actions/setup-java@v2
|
|
||||||
with:
|
|
||||||
distribution: 'temurin'
|
|
||||||
cache: 'gradle'
|
|
||||||
java-version: 17
|
|
||||||
- name: Set up Node.js
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: 18
|
|
||||||
- uses: pnpm/action-setup@v2.0.1
|
|
||||||
name: Install pnpm
|
|
||||||
id: pnpm-install
|
|
||||||
with:
|
|
||||||
version: 8
|
|
||||||
run_install: false
|
|
||||||
- name: Get pnpm store directory
|
|
||||||
id: pnpm-cache
|
|
||||||
run: |
|
|
||||||
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
|
|
||||||
- uses: actions/cache@v3
|
|
||||||
name: Setup pnpm cache
|
|
||||||
with:
|
|
||||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
|
||||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/widget/pnpm-lock.yaml') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-pnpm-store-
|
|
||||||
- name: Install Frontend Dependencies
|
|
||||||
run: |
|
|
||||||
./gradlew pnpmInstall
|
|
||||||
- name: Build with Gradle
|
|
||||||
run: |
|
|
||||||
# Set the version with tag name when releasing
|
|
||||||
version=${{ github.event.release.tag_name }}
|
|
||||||
version=${version#v}
|
|
||||||
sed -i "s/version=.*-SNAPSHOT$/version=$version/1" gradle.properties
|
|
||||||
./gradlew clean build -x test
|
|
||||||
- name: Archive plugin-s3 jar
|
|
||||||
uses: actions/upload-artifact@v2
|
|
||||||
with:
|
|
||||||
name: plugin-s3
|
|
||||||
path: |
|
|
||||||
build/libs/*.jar
|
|
||||||
retention-days: 1
|
|
||||||
|
|
||||||
github-release:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: build
|
|
||||||
if: github.event_name == 'release'
|
|
||||||
steps:
|
|
||||||
- name: Download plugin-s3 jar
|
|
||||||
uses: actions/download-artifact@v2
|
|
||||||
with:
|
|
||||||
name: plugin-s3
|
|
||||||
path: build/libs
|
|
||||||
- name: Get Name of Artifact
|
|
||||||
id: get_artifact
|
|
||||||
run: |
|
|
||||||
ARTIFACT_PATHNAME=$(ls build/libs/*.jar | head -n 1)
|
|
||||||
ARTIFACT_NAME=$(basename ${ARTIFACT_PATHNAME})
|
|
||||||
echo "Artifact pathname: ${ARTIFACT_PATHNAME}"
|
|
||||||
echo "Artifact name: ${ARTIFACT_NAME}"
|
|
||||||
echo "ARTIFACT_PATHNAME=${ARTIFACT_PATHNAME}" >> $GITHUB_ENV
|
|
||||||
echo "ARTIFACT_NAME=${ARTIFACT_NAME}" >> $GITHUB_ENV
|
|
||||||
echo "RELEASE_ID=${{ github.event.release.id }}" >> $GITHUB_ENV
|
|
||||||
- name: Upload a Release Asset
|
|
||||||
uses: actions/github-script@v2
|
|
||||||
if: github.event_name == 'release'
|
|
||||||
with:
|
|
||||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
|
||||||
script: |
|
|
||||||
console.log('environment', process.versions);
|
|
||||||
|
|
||||||
const fs = require('fs').promises;
|
|
||||||
|
|
||||||
const { repo: { owner, repo }, sha } = context;
|
|
||||||
console.log({ owner, repo, sha });
|
|
||||||
|
|
||||||
const releaseId = process.env.RELEASE_ID
|
|
||||||
const artifactPathName = process.env.ARTIFACT_PATHNAME
|
|
||||||
const artifactName = process.env.ARTIFACT_NAME
|
|
||||||
console.log('Releasing', releaseId, artifactPathName, artifactName)
|
|
||||||
|
|
||||||
await github.repos.uploadReleaseAsset({
|
|
||||||
owner, repo,
|
|
||||||
release_id: releaseId,
|
|
||||||
name: artifactName,
|
|
||||||
data: await fs.readFile(artifactPathName)
|
|
||||||
});
|
|
||||||
|
|
||||||
app-store-release:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: build
|
|
||||||
if: github.event_name == 'release'
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
submodules: true
|
|
||||||
- name: Download plugin-s3 jar
|
|
||||||
uses: actions/download-artifact@v2
|
|
||||||
with:
|
|
||||||
name: plugin-s3
|
|
||||||
path: build/libs
|
|
||||||
- name: Sync to Halo App Store
|
|
||||||
uses: halo-sigs/app-store-release-action@main
|
|
||||||
with:
|
|
||||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
|
||||||
app-id: ${{secrets.APP_ID}}
|
|
||||||
release-id: ${{ github.event.release.id }}
|
|
||||||
assets-dir: "build/libs"
|
|
||||||
halo-username: ${{ secrets.HALO_USERNAME }}
|
|
||||||
halo-password: ${{ secrets.HALO_PASSWORD }}
|
|
@@ -16,7 +16,7 @@ repositories {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation platform('run.halo.tools.platform:plugin:2.12.0-SNAPSHOT')
|
implementation platform('run.halo.tools.platform:plugin:2.14.0-SNAPSHOT')
|
||||||
compileOnly 'run.halo.app:api'
|
compileOnly 'run.halo.app:api'
|
||||||
|
|
||||||
implementation platform('software.amazon.awssdk:bom:2.19.8')
|
implementation platform('software.amazon.awssdk:bom:2.19.8')
|
||||||
@@ -36,7 +36,7 @@ configurations.runtimeClasspath {
|
|||||||
|
|
||||||
|
|
||||||
halo {
|
halo {
|
||||||
version = '2.12.1'
|
version = '2.14.0'
|
||||||
}
|
}
|
||||||
|
|
||||||
haloPlugin {
|
haloPlugin {
|
||||||
@@ -59,6 +59,10 @@ task buildFrontend(type: PnpmTask) {
|
|||||||
args = ['build']
|
args = ['build']
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.named('buildFrontend') {
|
||||||
|
dependsOn 'pnpmInstall'
|
||||||
|
}
|
||||||
|
|
||||||
build {
|
build {
|
||||||
// build frontend before build
|
// build frontend before build
|
||||||
tasks.getByName('compileJava').dependsOn('buildFrontend')
|
tasks.getByName('compileJava').dependsOn('buildFrontend')
|
||||||
|
@@ -1 +1 @@
|
|||||||
version=1.8.0-SNAPSHOT
|
version=1.9.0-SNAPSHOT
|
||||||
|
@@ -6,6 +6,6 @@ import lombok.experimental.UtilityClass;
|
|||||||
public class FilePathUtils {
|
public class FilePathUtils {
|
||||||
|
|
||||||
public static String getFilePathByPlaceholder(String filePath) {
|
public static String getFilePathByPlaceholder(String filePath) {
|
||||||
return PlaceholderReplacer.replacePlaceholders(filePath, "");
|
return PlaceholderReplacer.replacePlaceholders(filePath, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,13 +1,12 @@
|
|||||||
package run.halo.s3os;
|
package run.halo.s3os;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class LinkRequest {
|
public class LinkRequest {
|
||||||
private String policyName;
|
private String policyName;
|
||||||
private List<String> objectKeys;
|
private Set<String> objectKeys;
|
||||||
}
|
}
|
@@ -0,0 +1,123 @@
|
|||||||
|
package run.halo.s3os;
|
||||||
|
|
||||||
|
import static run.halo.s3os.S3OsAttachmentHandler.MULTIPART_MIN_PART_SIZE;
|
||||||
|
import static run.halo.s3os.S3OsAttachmentHandler.checkResult;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.core.io.DefaultResourceLoader;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.core.io.buffer.DataBuffer;
|
||||||
|
import org.springframework.core.io.buffer.DataBufferUtils;
|
||||||
|
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import run.halo.app.infra.utils.PathUtils;
|
||||||
|
import run.halo.app.plugin.ApiVersion;
|
||||||
|
import software.amazon.awssdk.services.s3.model.CompleteMultipartUploadRequest;
|
||||||
|
import software.amazon.awssdk.services.s3.model.CompletedMultipartUpload;
|
||||||
|
import software.amazon.awssdk.services.s3.model.CreateMultipartUploadRequest;
|
||||||
|
import software.amazon.awssdk.services.s3.model.HeadObjectRequest;
|
||||||
|
import software.amazon.awssdk.utils.SdkAutoCloseable;
|
||||||
|
|
||||||
|
@ApiVersion("s3os.halo.run/v1alpha1")
|
||||||
|
@RestController
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class PolicyConfigValidationController {
|
||||||
|
private final S3OsAttachmentHandler handler;
|
||||||
|
|
||||||
|
@PostMapping("/policies/s3/validation")
|
||||||
|
public Mono<Void> validatePolicyConfig(@RequestBody S3OsProperties properties) {
|
||||||
|
var filename = "halo-s3-plugin-test-file-" + System.currentTimeMillis() + ".jpg";
|
||||||
|
var content = readImage();
|
||||||
|
return Mono.using(() -> handler.buildS3Client(properties),
|
||||||
|
client -> {
|
||||||
|
var uploadState =
|
||||||
|
new S3OsAttachmentHandler.UploadState(properties, filename, false);
|
||||||
|
|
||||||
|
return handler.checkFileExistsAndRename(uploadState, client)
|
||||||
|
// init multipart upload
|
||||||
|
.flatMap(state -> Mono.fromCallable(() -> client.createMultipartUpload(
|
||||||
|
CreateMultipartUploadRequest.builder()
|
||||||
|
.bucket(properties.getBucket())
|
||||||
|
.contentType(state.contentType)
|
||||||
|
.key(state.objectKey)
|
||||||
|
.build())))
|
||||||
|
.doOnNext((response) -> {
|
||||||
|
checkResult(response, "createMultipartUpload");
|
||||||
|
uploadState.uploadId = response.uploadId();
|
||||||
|
})
|
||||||
|
.thenMany(handler.reshape(content, MULTIPART_MIN_PART_SIZE))
|
||||||
|
// buffer to part
|
||||||
|
.windowUntil((buffer) -> {
|
||||||
|
uploadState.buffered += buffer.readableByteCount();
|
||||||
|
if (uploadState.buffered >= MULTIPART_MIN_PART_SIZE) {
|
||||||
|
uploadState.buffered = 0;
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// upload part
|
||||||
|
.concatMap((window) -> window.collectList().flatMap((bufferList) -> {
|
||||||
|
var buffer = S3OsAttachmentHandler.concatBuffers(bufferList);
|
||||||
|
return handler.uploadPart(uploadState, buffer, client);
|
||||||
|
}))
|
||||||
|
.reduce(uploadState, (state, completedPart) -> {
|
||||||
|
state.completedParts.put(completedPart.partNumber(), completedPart);
|
||||||
|
return state;
|
||||||
|
})
|
||||||
|
// complete multipart upload
|
||||||
|
.flatMap((state) -> Mono.just(client.completeMultipartUpload(
|
||||||
|
CompleteMultipartUploadRequest
|
||||||
|
.builder()
|
||||||
|
.bucket(properties.getBucket())
|
||||||
|
.uploadId(state.uploadId)
|
||||||
|
.multipartUpload(CompletedMultipartUpload.builder()
|
||||||
|
.parts(state.completedParts.values())
|
||||||
|
.build())
|
||||||
|
.key(state.objectKey)
|
||||||
|
.build())
|
||||||
|
))
|
||||||
|
// get object metadata
|
||||||
|
.flatMap((response) -> {
|
||||||
|
checkResult(response, "completeUpload");
|
||||||
|
return Mono.just(client.headObject(
|
||||||
|
HeadObjectRequest.builder()
|
||||||
|
.bucket(properties.getBucket())
|
||||||
|
.key(uploadState.objectKey)
|
||||||
|
.build()
|
||||||
|
));
|
||||||
|
})
|
||||||
|
// check object metadata
|
||||||
|
.doOnNext((response) -> {
|
||||||
|
checkResult(response, "headObject");
|
||||||
|
})
|
||||||
|
// delete object
|
||||||
|
.flatMap((response) -> Mono.just(client.deleteObject(
|
||||||
|
software.amazon.awssdk.services.s3.model.DeleteObjectRequest.builder()
|
||||||
|
.bucket(properties.getBucket())
|
||||||
|
.key(uploadState.objectKey)
|
||||||
|
.build()
|
||||||
|
)))
|
||||||
|
.doOnNext((response) -> checkResult(response, "deleteObject"))
|
||||||
|
.then();
|
||||||
|
},
|
||||||
|
SdkAutoCloseable::close)
|
||||||
|
.onErrorMap(S3ExceptionHandler::map);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Flux<DataBuffer> readImage() {
|
||||||
|
DefaultResourceLoader resourceLoader = new DefaultResourceLoader(this.getClass()
|
||||||
|
.getClassLoader());
|
||||||
|
String path = PathUtils.combinePath("validation.jpg");
|
||||||
|
String simplifyPath = StringUtils.cleanPath(path);
|
||||||
|
Resource resource = resourceLoader.getResource(simplifyPath);
|
||||||
|
return DataBufferUtils.read(resource, new DefaultDataBufferFactory(), 1024);
|
||||||
|
}
|
||||||
|
}
|
@@ -1,30 +1,22 @@
|
|||||||
package run.halo.s3os;
|
package run.halo.s3os;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.core.extension.attachment.Attachment;
|
|
||||||
import run.halo.app.core.extension.attachment.Policy;
|
import run.halo.app.core.extension.attachment.Policy;
|
||||||
import run.halo.app.extension.ReactiveExtensionClient;
|
|
||||||
import run.halo.app.plugin.ApiVersion;
|
import run.halo.app.plugin.ApiVersion;
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
|
|
||||||
@ApiVersion("s3os.halo.run/v1alpha1")
|
@ApiVersion("s3os.halo.run/v1alpha1")
|
||||||
@RestController
|
@RestController
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class S3LinkController {
|
public class S3LinkController {
|
||||||
private final S3LinkService s3LinkService;
|
private final S3LinkService s3LinkService;
|
||||||
private final ReactiveExtensionClient client;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Map of linking file, used as a lock, key is policyName/objectKey, value is policyName/objectKey.
|
|
||||||
*/
|
|
||||||
private final Map<String, Object> linkingFile = new ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
@GetMapping("/policies/s3")
|
@GetMapping("/policies/s3")
|
||||||
public Flux<Policy> listS3Policies() {
|
public Flux<Policy> listS3Policies() {
|
||||||
@@ -48,38 +40,7 @@ public class S3LinkController {
|
|||||||
|
|
||||||
@PostMapping("/attachments/link")
|
@PostMapping("/attachments/link")
|
||||||
public Mono<LinkResult> addAttachmentRecord(@RequestBody LinkRequest linkRequest) {
|
public Mono<LinkResult> addAttachmentRecord(@RequestBody LinkRequest linkRequest) {
|
||||||
return Flux.fromIterable(linkRequest.getObjectKeys())
|
return s3LinkService.addAttachmentRecords(linkRequest.getPolicyName(),
|
||||||
.filter(objectKey -> linkingFile.put(linkRequest.getPolicyName() + "/" + objectKey,
|
linkRequest.getObjectKeys());
|
||||||
linkRequest.getPolicyName() + "/" + objectKey) == null)
|
|
||||||
.collectList()
|
|
||||||
.flatMap(operableObjectKeys -> client.list(Attachment.class,
|
|
||||||
attachment -> Objects.equals(attachment.getSpec().getPolicyName(),
|
|
||||||
linkRequest.getPolicyName())
|
|
||||||
&& StringUtils.isNotEmpty(attachment.getMetadata().getAnnotations()
|
|
||||||
.get(S3OsAttachmentHandler.OBJECT_KEY))
|
|
||||||
&& linkRequest.getObjectKeys().contains(attachment.getMetadata()
|
|
||||||
.getAnnotations().get(S3OsAttachmentHandler.OBJECT_KEY)),
|
|
||||||
null)
|
|
||||||
.collectList()
|
|
||||||
.flatMap(existingAttachments -> Flux.fromIterable(linkRequest.getObjectKeys())
|
|
||||||
.flatMap((objectKey) -> {
|
|
||||||
if (operableObjectKeys.contains(objectKey) && existingAttachments.stream()
|
|
||||||
.noneMatch(attachment -> Objects.equals(
|
|
||||||
attachment.getMetadata().getAnnotations().get(
|
|
||||||
S3OsAttachmentHandler.OBJECT_KEY), objectKey))) {
|
|
||||||
return s3LinkService
|
|
||||||
.addAttachmentRecord(linkRequest.getPolicyName(), objectKey)
|
|
||||||
.onErrorResume((throwable) -> Mono.just(
|
|
||||||
new LinkResult.LinkResultItem(objectKey, false,
|
|
||||||
throwable.getMessage())));
|
|
||||||
} else {
|
|
||||||
return Mono.just(new LinkResult.LinkResultItem(objectKey, false,
|
|
||||||
"附件库中已存在该对象"));
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.doOnNext(linkResultItem -> linkingFile.remove(
|
|
||||||
linkRequest.getPolicyName() + "/" + linkResultItem.getObjectKey()))
|
|
||||||
.collectList()
|
|
||||||
.map(LinkResult::new)));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
package run.halo.s3os;
|
package run.halo.s3os;
|
||||||
|
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.core.extension.attachment.Policy;
|
import run.halo.app.core.extension.attachment.Policy;
|
||||||
@@ -11,7 +12,7 @@ public interface S3LinkService {
|
|||||||
Mono<S3ListResult> listObjects(String policyName, String continuationToken,
|
Mono<S3ListResult> listObjects(String policyName, String continuationToken,
|
||||||
Integer pageSize);
|
Integer pageSize);
|
||||||
|
|
||||||
Mono<LinkResult.LinkResultItem> addAttachmentRecord(String policyName, String objectKey);
|
Mono<LinkResult> addAttachmentRecords(String policyName, Set<String> objectKeys);
|
||||||
|
|
||||||
Mono<S3ListResult> listObjectsUnlinked(String policyName, String continuationToken,
|
Mono<S3ListResult> listObjectsUnlinked(String policyName, String continuationToken,
|
||||||
String continuationObject, Integer pageSize);
|
String continuationObject, Integer pageSize);
|
||||||
|
@@ -4,6 +4,9 @@ import static run.halo.s3os.S3OsAttachmentHandler.OBJECT_KEY;
|
|||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
@@ -23,7 +26,11 @@ import reactor.core.scheduler.Schedulers;
|
|||||||
import run.halo.app.core.extension.attachment.Attachment;
|
import run.halo.app.core.extension.attachment.Attachment;
|
||||||
import run.halo.app.core.extension.attachment.Policy;
|
import run.halo.app.core.extension.attachment.Policy;
|
||||||
import run.halo.app.extension.ConfigMap;
|
import run.halo.app.extension.ConfigMap;
|
||||||
|
import run.halo.app.extension.ListOptions;
|
||||||
|
import run.halo.app.extension.MetadataUtil;
|
||||||
import run.halo.app.extension.ReactiveExtensionClient;
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
|
import run.halo.app.extension.index.query.QueryFactory;
|
||||||
|
import run.halo.app.extension.router.selector.FieldSelector;
|
||||||
import software.amazon.awssdk.services.s3.S3Client;
|
import software.amazon.awssdk.services.s3.S3Client;
|
||||||
import software.amazon.awssdk.services.s3.model.HeadObjectRequest;
|
import software.amazon.awssdk.services.s3.model.HeadObjectRequest;
|
||||||
import software.amazon.awssdk.services.s3.model.ListObjectsV2Request;
|
import software.amazon.awssdk.services.s3.model.ListObjectsV2Request;
|
||||||
@@ -37,6 +44,11 @@ public class S3LinkServiceImpl implements S3LinkService {
|
|||||||
private final ReactiveExtensionClient client;
|
private final ReactiveExtensionClient client;
|
||||||
private final S3OsAttachmentHandler handler;
|
private final S3OsAttachmentHandler handler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map of linking file, used as a lock, key is policyName/objectKey, value is policyName/objectKey.
|
||||||
|
*/
|
||||||
|
private final Map<String, Object> linkingFile = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Flux<Policy> listS3Policies() {
|
public Flux<Policy> listS3Policies() {
|
||||||
@@ -73,9 +85,10 @@ public class S3LinkServiceImpl implements S3LinkService {
|
|||||||
.stream().map(S3ListResult.ObjectVo::fromS3Object)
|
.stream().map(S3ListResult.ObjectVo::fromS3Object)
|
||||||
.filter(objectVo -> !objectVo.getKey().endsWith("/"))
|
.filter(objectVo -> !objectVo.getKey().endsWith("/"))
|
||||||
.collect(Collectors.toMap(S3ListResult.ObjectVo::getKey, o -> o));
|
.collect(Collectors.toMap(S3ListResult.ObjectVo::getKey, o -> o));
|
||||||
return client.list(Attachment.class,
|
ListOptions listOptions = new ListOptions();
|
||||||
attachment -> policyName.equals(
|
listOptions.setFieldSelector(
|
||||||
attachment.getSpec().getPolicyName()), null)
|
FieldSelector.of(QueryFactory.equal("spec.policyName", policyName)));
|
||||||
|
return client.listAll(Attachment.class, listOptions, null)
|
||||||
.doOnNext(attachment -> {
|
.doOnNext(attachment -> {
|
||||||
S3ListResult.ObjectVo objectVo =
|
S3ListResult.ObjectVo objectVo =
|
||||||
objectVos.get(attachment.getMetadata().getAnnotations()
|
objectVos.get(attachment.getMetadata().getAnnotations()
|
||||||
@@ -95,6 +108,59 @@ public class S3LinkServiceImpl implements S3LinkService {
|
|||||||
.onErrorMap(S3ExceptionHandler::map);
|
.onErrorMap(S3ExceptionHandler::map);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<LinkResult> addAttachmentRecords(String policyName, Set<String> objectKeys) {
|
||||||
|
return getOperableObjectKeys(objectKeys, policyName)
|
||||||
|
.flatMap(operableObjectKeys -> getExistingAttachments(objectKeys, policyName)
|
||||||
|
.flatMap(existingAttachments -> getLinkResultItems(objectKeys, operableObjectKeys,
|
||||||
|
existingAttachments, policyName)
|
||||||
|
.collectList()
|
||||||
|
.map(LinkResult::new)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mono<Set<String>> getOperableObjectKeys(Set<String> objectKeys, String policyName) {
|
||||||
|
return Flux.fromIterable(objectKeys)
|
||||||
|
.filter(objectKey ->
|
||||||
|
linkingFile.put(policyName + "/" + objectKey, policyName + "/" + objectKey) == null)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mono<Set<String>> getExistingAttachments(Set<String> objectKeys,
|
||||||
|
String policyName) {
|
||||||
|
ListOptions listOptions = new ListOptions();
|
||||||
|
listOptions.setFieldSelector(
|
||||||
|
FieldSelector.of(QueryFactory.equal("spec.policyName", policyName)));
|
||||||
|
return client.listAll(Attachment.class, listOptions, null)
|
||||||
|
.filter(attachment -> StringUtils.isNotBlank(
|
||||||
|
MetadataUtil.nullSafeAnnotations(attachment).get(S3OsAttachmentHandler.OBJECT_KEY))
|
||||||
|
&& objectKeys.contains(
|
||||||
|
MetadataUtil.nullSafeAnnotations(attachment).get(S3OsAttachmentHandler.OBJECT_KEY)))
|
||||||
|
.map(attachment -> MetadataUtil.nullSafeAnnotations(attachment)
|
||||||
|
.get(S3OsAttachmentHandler.OBJECT_KEY))
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Flux<LinkResult.LinkResultItem> getLinkResultItems(Set<String> objectKeys,
|
||||||
|
Set<String> operableObjectKeys,
|
||||||
|
Set<String> existingAttachments,
|
||||||
|
String policyName) {
|
||||||
|
return Flux.fromIterable(objectKeys)
|
||||||
|
.flatMap((objectKey) -> {
|
||||||
|
if (operableObjectKeys.contains(objectKey) &&
|
||||||
|
!existingAttachments.contains(objectKey)) {
|
||||||
|
return addAttachmentRecord(policyName, objectKey)
|
||||||
|
.onErrorResume((throwable) -> Mono.just(
|
||||||
|
new LinkResult.LinkResultItem(objectKey, false,
|
||||||
|
throwable.getMessage())));
|
||||||
|
} else {
|
||||||
|
return Mono.just(
|
||||||
|
new LinkResult.LinkResultItem(objectKey, false, "附件库中已存在该对象"));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.doFinally(signalType -> operableObjectKeys.forEach(
|
||||||
|
objectKey -> linkingFile.remove(policyName + "/" + objectKey)));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<S3ListResult> listObjectsUnlinked(String policyName, String continuationToken,
|
public Mono<S3ListResult> listObjectsUnlinked(String policyName, String continuationToken,
|
||||||
String continuationObject, Integer pageSize) {
|
String continuationObject, Integer pageSize) {
|
||||||
@@ -151,8 +217,6 @@ public class S3LinkServiceImpl implements S3LinkService {
|
|||||||
record TokenState(String currToken, String nextToken) {
|
record TokenState(String currToken, String nextToken) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Mono<LinkResult.LinkResultItem> addAttachmentRecord(String policyName,
|
public Mono<LinkResult.LinkResultItem> addAttachmentRecord(String policyName,
|
||||||
String objectKey) {
|
String objectKey) {
|
||||||
return authenticationConsumer(authentication -> client.fetch(Policy.class, policyName)
|
return authenticationConsumer(authentication -> client.fetch(Policy.class, policyName)
|
||||||
|
@@ -390,7 +390,7 @@ public class S3OsAttachmentHandler implements AttachmentHandler {
|
|||||||
SdkAutoCloseable::close);
|
SdkAutoCloseable::close);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Mono<UploadState> checkFileExistsAndRename(UploadState uploadState,
|
Mono<UploadState> checkFileExistsAndRename(UploadState uploadState,
|
||||||
S3Client s3client) {
|
S3Client s3client) {
|
||||||
return Mono.defer(() -> {
|
return Mono.defer(() -> {
|
||||||
// deduplication of uploading files
|
// deduplication of uploading files
|
||||||
@@ -437,7 +437,7 @@ public class S3OsAttachmentHandler implements AttachmentHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private Mono<CompletedPart> uploadPart(UploadState uploadState, ByteBuffer buffer,
|
Mono<CompletedPart> uploadPart(UploadState uploadState, ByteBuffer buffer,
|
||||||
S3Client s3client) {
|
S3Client s3client) {
|
||||||
final int partNumber = ++uploadState.partCounter;
|
final int partNumber = ++uploadState.partCounter;
|
||||||
return Mono.just(s3client.uploadPart(UploadPartRequest.builder()
|
return Mono.just(s3client.uploadPart(UploadPartRequest.builder()
|
||||||
@@ -457,7 +457,7 @@ public class S3OsAttachmentHandler implements AttachmentHandler {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void checkResult(SdkResponse result, String operation) {
|
static void checkResult(SdkResponse result, String operation) {
|
||||||
log.info("operation: {}, result: {}", operation, result);
|
log.info("operation: {}, result: {}", operation, result);
|
||||||
if (result.sdkHttpResponse() == null || !result.sdkHttpResponse().isSuccessful()) {
|
if (result.sdkHttpResponse() == null || !result.sdkHttpResponse().isSuccessful()) {
|
||||||
log.error("Failed to upload object, response: {}", result.sdkHttpResponse());
|
log.error("Failed to upload object, response: {}", result.sdkHttpResponse());
|
||||||
@@ -465,7 +465,7 @@ public class S3OsAttachmentHandler implements AttachmentHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ByteBuffer concatBuffers(List<DataBuffer> buffers) {
|
static ByteBuffer concatBuffers(List<DataBuffer> buffers) {
|
||||||
int partSize = 0;
|
int partSize = 0;
|
||||||
for (DataBuffer b : buffers) {
|
for (DataBuffer b : buffers) {
|
||||||
partSize += b.readableByteCount();
|
partSize += b.readableByteCount();
|
||||||
|
@@ -3,7 +3,7 @@ kind: PolicyTemplate
|
|||||||
metadata:
|
metadata:
|
||||||
name: s3os
|
name: s3os
|
||||||
spec:
|
spec:
|
||||||
displayName: S3 Object Storage
|
displayName: S3 对象存储
|
||||||
settingName: s3os-policy-template-setting
|
settingName: s3os-policy-template-setting
|
||||||
---
|
---
|
||||||
apiVersion: v1alpha1
|
apiVersion: v1alpha1
|
||||||
@@ -14,6 +14,10 @@ spec:
|
|||||||
forms:
|
forms:
|
||||||
- group: default
|
- group: default
|
||||||
formSchema:
|
formSchema:
|
||||||
|
- $formkit: verificationForm
|
||||||
|
action: "/apis/s3os.halo.run/v1alpha1/policies/s3/validation"
|
||||||
|
label: 对象存储验证
|
||||||
|
children:
|
||||||
- $formkit: text
|
- $formkit: text
|
||||||
name: bucket
|
name: bucket
|
||||||
label: Bucket 桶名称
|
label: Bucket 桶名称
|
||||||
|
@@ -39,3 +39,17 @@ rules:
|
|||||||
- apiGroups: [ "s3os.halo.run" ]
|
- apiGroups: [ "s3os.halo.run" ]
|
||||||
resources: [ "attachments" ]
|
resources: [ "attachments" ]
|
||||||
verbs: [ "delete" ]
|
verbs: [ "delete" ]
|
||||||
|
---
|
||||||
|
apiVersion: v1alpha1
|
||||||
|
kind: "Role"
|
||||||
|
metadata:
|
||||||
|
name: role-template-s3os-policy-config-validation
|
||||||
|
labels:
|
||||||
|
halo.run/role-template: "true"
|
||||||
|
halo.run/hidden: "true"
|
||||||
|
rbac.authorization.halo.run/aggregate-to-role-template-manage-configmaps: "true"
|
||||||
|
rules:
|
||||||
|
- apiGroups: ["s3os.halo.run"]
|
||||||
|
resources: ["policies/validation"]
|
||||||
|
resourceNames: ["s3"]
|
||||||
|
verbs: [ "create" ]
|
||||||
|
@@ -7,8 +7,5 @@ spec:
|
|||||||
- group: basic
|
- group: basic
|
||||||
label: 使用提示
|
label: 使用提示
|
||||||
formSchema:
|
formSchema:
|
||||||
- $formkit: text
|
- $el: p
|
||||||
help: 请前往 “附件 - 存储策略” 添加策略
|
children: 请前往 “附件 - 存储策略” 添加策略
|
||||||
label: 此处不用设置,请前往 “附件 - 存储策略” 添加策略
|
|
||||||
name: text
|
|
||||||
placeholder: 此处不用设置,请前往 “附件 - 存储策略” 添加策略
|
|
||||||
|
@@ -4,14 +4,16 @@ metadata:
|
|||||||
name: PluginS3ObjectStorage
|
name: PluginS3ObjectStorage
|
||||||
spec:
|
spec:
|
||||||
enabled: true
|
enabled: true
|
||||||
requires: ">=2.12.0"
|
requires: ">=2.14.0"
|
||||||
author:
|
author:
|
||||||
name: Halo OSS Team
|
name: Halo
|
||||||
website: https://github.com/halo-dev
|
website: https://github.com/halo-dev
|
||||||
logo: data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAABUdJREFUeF7tmmtoXFUQx3+zMYGi/RJRULRFsL6oim2klVK0CrVqKxjoJ0Vt1bR7t6lURagiFlGpFPogydklVo1i/VB84QMfqFijpflQrI+IYj7FF+0HMbUEI82O3NNtzG6y956bvXd3IXvgksCd+c/M/8w598ycFWb5kFkePw0CGhkwyxloLIFZngCNTbCxBBpLoI4Y0A6aydNKC62kaAXGSDHCv4zQzIh0MRa3uzVdAppmMSmWoVyL0IZyWUiA3wL7gI/FcCQOMmpCQCHwDpSOCoIYQOmWLK9WgFHdr4Bu4wyOsR14uBKni3SFdxhnl+T4fCaYVcsAzbCEPNsRbpiJow46u8TwkINcCX9RNWYgr2nWIrwAzJ2BehSVAVpYJbv5y1Up8QwoBL/f1aGY5JaJ4aALVqIEqMc24EkXRxKQmSeGX8JwEyNAPR4AesMcSPD9ECdpk15GgmwkQoB2spxxPgWaEwzQBfptMdxRfQI8PgJWuniYuIyyQbLlMzH2DFCPDUAu8cDcDRwWQ1s58VgJ0E7OYZyvgAXu/lVBMiAL4iXA4z5gbxVCimpiQAxLp1OKl4AMfSj3RPWuKvJ5FkmOr0ttxUuAx9/AWVUJKKoR5QnJ8nRsBOh65tJiy9nFYJ+fwB586nUcEsN1FROgHncDG2EqWIWR/wpcUCFGsPooc6SPfyYLOS8BzbAI5RlgVQJObrXdH9iZAPb/kCkul25+jEyAejboDxJyLiMG42Orx2fAioTs+LC3iOHDSARohs0oexJxStgtPWw5jZ34IUpJS7b4kBa4BDTDTSifJBI8vC+G1ZOxdQtzGGMIOD8hm1vF2I7UxAgmwLNpn8SaHyTPjZLjWGmg6tl9YCIrYiVCuVOyvOZEQGG3fzlWB06BjZJipXTbI/OUoWmuQvgmAbs+5HIxfOlKwCFgSQKOrBNDXxCuerwH3Ba77ZPMl16GQwnQjSwkxXexO6A8JdnwDpF6tANvxG1fzNQu+LR7QEKtrFfEuNcJ6vEzcHGMJEzZdH3scgRkC6e9eOwr/Qi3iuGEK6B6PAo85yofKid40oMfV9EoR8CbENxKCjVYvNCWSg8DUXQKn8TRKDqBstOs/6AMiLelVaYUDQtO0zyPcH+YnMP7d8Vw+3Ry5TLgJeBeB2BXEX8mDwDHEUZQWzafEBNcPca2FwlrpMd+WaaM6QnI8CyKX6AkN/KsmHyfZ9voQjfKUbDPOMoCxF6TVzLKzn75JZBmOcIXlVgN0y39JGka/yOVDtOL/D5g9ssS4L9Qz5aNl0Y26KZwRAzXTBZVj8PAIjd1Z6ntYoIzuWwtoBkeK9T/ztacBUurwA1cQRODzvpugvvEcFeYaHkCHuFMRvGPwwvDQCK+P0qexZLjt9N6mmE9am+P4xnKn5LlbBew4GowzTqEF12AnGXytEuOtyaC30aKY7YRcr0zRrDg92K40hUrtCWmGfajrHUFDJHzf9tzc8na9zu1j8eCL/RKj72Zch6hBPhI6tEFbHJGnV5wSEzxjVGMrbZBlB2SJXL57kRAgQS/G7wDODcyEUKX9LC5ZOb9+wO/lV5J9+c4sIsmdkoX/v+RhzMBloRNXESeB8E+4UNtSbtHsvSXCqt/1oB2xNYc88PBiiT8uuIgTeyVLn6IqFskHomAiY0rwyUoa1BWI3YGzyu8+wPld1K8Tp5+yeL/ri90aCdXM26JuBCYN/GcOjYPIwzbv3CYJg5IF/4dQixjRgTEYrlOQBoE1MlE1MyNRgbUjPo6MdzIgDqZiJq50ciAmlFfJ4YbGVAnE1EzN2Z9BvwH8mZfUOuuxhIAAAAASUVORK5CYII=
|
logo: data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAABUdJREFUeF7tmmtoXFUQx3+zMYGi/RJRULRFsL6oim2klVK0CrVqKxjoJ0Vt1bR7t6lURagiFlGpFPogydklVo1i/VB84QMfqFijpflQrI+IYj7FF+0HMbUEI82O3NNtzG6y956bvXd3IXvgksCd+c/M/8w598ycFWb5kFkePw0CGhkwyxloLIFZngCNTbCxBBpLoI4Y0A6aydNKC62kaAXGSDHCv4zQzIh0MRa3uzVdAppmMSmWoVyL0IZyWUiA3wL7gI/FcCQOMmpCQCHwDpSOCoIYQOmWLK9WgFHdr4Bu4wyOsR14uBKni3SFdxhnl+T4fCaYVcsAzbCEPNsRbpiJow46u8TwkINcCX9RNWYgr2nWIrwAzJ2BehSVAVpYJbv5y1Up8QwoBL/f1aGY5JaJ4aALVqIEqMc24EkXRxKQmSeGX8JwEyNAPR4AesMcSPD9ECdpk15GgmwkQoB2spxxPgWaEwzQBfptMdxRfQI8PgJWuniYuIyyQbLlMzH2DFCPDUAu8cDcDRwWQ1s58VgJ0E7OYZyvgAXu/lVBMiAL4iXA4z5gbxVCimpiQAxLp1OKl4AMfSj3RPWuKvJ5FkmOr0ttxUuAx9/AWVUJKKoR5QnJ8nRsBOh65tJiy9nFYJ+fwB586nUcEsN1FROgHncDG2EqWIWR/wpcUCFGsPooc6SPfyYLOS8BzbAI5RlgVQJObrXdH9iZAPb/kCkul25+jEyAejboDxJyLiMG42Orx2fAioTs+LC3iOHDSARohs0oexJxStgtPWw5jZ34IUpJS7b4kBa4BDTDTSifJBI8vC+G1ZOxdQtzGGMIOD8hm1vF2I7UxAgmwLNpn8SaHyTPjZLjWGmg6tl9YCIrYiVCuVOyvOZEQGG3fzlWB06BjZJipXTbI/OUoWmuQvgmAbs+5HIxfOlKwCFgSQKOrBNDXxCuerwH3Ba77ZPMl16GQwnQjSwkxXexO6A8JdnwDpF6tANvxG1fzNQu+LR7QEKtrFfEuNcJ6vEzcHGMJEzZdH3scgRkC6e9eOwr/Qi3iuGEK6B6PAo85yofKid40oMfV9EoR8CbENxKCjVYvNCWSg8DUXQKn8TRKDqBstOs/6AMiLelVaYUDQtO0zyPcH+YnMP7d8Vw+3Ry5TLgJeBeB2BXEX8mDwDHEUZQWzafEBNcPca2FwlrpMd+WaaM6QnI8CyKX6AkN/KsmHyfZ9voQjfKUbDPOMoCxF6TVzLKzn75JZBmOcIXlVgN0y39JGka/yOVDtOL/D5g9ssS4L9Qz5aNl0Y26KZwRAzXTBZVj8PAIjd1Z6ntYoIzuWwtoBkeK9T/ztacBUurwA1cQRODzvpugvvEcFeYaHkCHuFMRvGPwwvDQCK+P0qexZLjt9N6mmE9am+P4xnKn5LlbBew4GowzTqEF12AnGXytEuOtyaC30aKY7YRcr0zRrDg92K40hUrtCWmGfajrHUFDJHzf9tzc8na9zu1j8eCL/RKj72Zch6hBPhI6tEFbHJGnV5wSEzxjVGMrbZBlB2SJXL57kRAgQS/G7wDODcyEUKX9LC5ZOb9+wO/lV5J9+c4sIsmdkoX/v+RhzMBloRNXESeB8E+4UNtSbtHsvSXCqt/1oB2xNYc88PBiiT8uuIgTeyVLn6IqFskHomAiY0rwyUoa1BWI3YGzyu8+wPld1K8Tp5+yeL/ri90aCdXM26JuBCYN/GcOjYPIwzbv3CYJg5IF/4dQixjRgTEYrlOQBoE1MlE1MyNRgbUjPo6MdzIgDqZiJq50ciAmlFfJ4YbGVAnE1EzN2Z9BvwH8mZfUOuuxhIAAAAASUVORK5CYII=
|
||||||
settingName: s3os-settings
|
settingName: s3os-settings
|
||||||
configMapName: s3os-configMap
|
configMapName: s3os-configMap
|
||||||
homepage: https://github.com/halo-dev/plugin-s3
|
homepage: https://www.halo.run/store/apps/app-Qxhpp
|
||||||
|
repo: https://github.com/halo-dev/plugin-s3
|
||||||
|
issues: https://github.com/halo-dev/plugin-s3/issues
|
||||||
displayName: "对象存储(Amazon S3 协议)"
|
displayName: "对象存储(Amazon S3 协议)"
|
||||||
description: "提供兼容 Amazon S3 协议的对象存储策略,兼容阿里云、腾讯云、七牛云等"
|
description: "提供兼容 Amazon S3 协议的对象存储策略,兼容阿里云、腾讯云、七牛云等"
|
||||||
license:
|
license:
|
||||||
|
BIN
src/main/resources/validation.jpg
Normal file
BIN
src/main/resources/validation.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
Reference in New Issue
Block a user