7 Commits

Author SHA1 Message Date
guqing
40f66ff665 chore: remove deprecated plugin wrapper (#161)
### What this PR does?
移除对已过时的 PluginWrapper 的引用,Halo 2.18.0 版本后将不在支持从 BasePlugin 中获取 PluginWrapper ,也不再支持依赖注入 PluginWrapper,使用 PluginContext 代替

see also https://github.com/halo-dev/halo/pull/6243 for more details

```release-note
None
```
2024-07-02 15:29:25 +00:00
longjuan
cb968de154 fix: apply natural ordering to policy and attachment listings (#160)
```release-note
None
```
传递unsorted以支持Halo 2.17版本
2024-07-02 15:27:25 +00:00
longjuan
bdd62cbe12 faet: Add file prefix search function in s3link (#153)
```release-note
在 S3关联 中支持文件名前缀搜索
```
fixes https://github.com/halo-dev/plugin-s3/issues/142
2024-06-16 12:30:45 +00:00
longjuan
2a26171fc4 Fix: Correct policy selector styling issue (#151)
```release-note
修复 S3 关联中策略选择器太短的问题
```
fixes #141
2024-05-14 02:08:33 +00:00
longjuan
6b62ce7aa4 Hide the permission of the verification interface (#147)
fixes https://github.com/halo-dev/plugin-s3/issues/146
```release-note
在角色创建中隐藏配置校验接口的权限
```
2024-05-06 10:34:54 +00:00
Ryan Wang
c60e31a033 chore: update plugin.yaml file (#138)
完善插件定义文件。

1. 将 homepage 字段指向 Halo 应用市场的地址。
2. 新增 repo 字段指向源码仓库地址。
3. 新增 issues 字段指向 GitHub issues 地址。

```release-note
None
```
2024-05-05 08:39:33 +00:00
Chenhe
7a9b0de0c6 improve usage tips in settings page (#140)
使用占位组件在设置页面展示信息并不恰当,并且此项目作为存储插件的 Example 工程易误导其他开发者。

调整为利用 FormKit `el` 特性实现。

问题背景:https://github.com/halo-dev/halo/issues/5802

**Preview - old**
<img width="373" alt="image" src="https://github.com/halo-dev/plugin-s3/assets/10266066/ac4aaede-6377-459f-a866-9bfe132906af">

**Preview - new**
<img width="373" alt="image" src="https://github.com/halo-dev/plugin-s3/assets/10266066/f884417c-045a-4396-84be-633d18ae36c9">

```release-note
优化调整提示的提示
```
2024-04-28 11:54:48 +00:00
10 changed files with 76 additions and 36 deletions

View File

@@ -10,6 +10,7 @@ export function getApisS3OsHaloRunV1Alpha1ObjectsByPolicyName(params: GetApisS3O
continuationObject: params.continuationObject, continuationObject: params.continuationObject,
pageSize: params.pageSize, pageSize: params.pageSize,
unlinked: params.unlinked, unlinked: params.unlinked,
filePrefix: params.filePrefix,
}; };
return request.get<DeepRequired<S3ListResult>>(`/apis/s3os.halo.run/v1alpha1/objects/${params.policyName}`, { return request.get<DeepRequired<S3ListResult>>(`/apis/s3os.halo.run/v1alpha1/objects/${params.policyName}`, {
params: paramsInput, params: paramsInput,
@@ -22,4 +23,5 @@ interface GetApisS3OsHaloRunV1Alpha1ObjectsByPolicyNameParams {
continuationObject?: any; continuationObject?: any;
pageSize: any; pageSize: any;
unlinked?: any; unlinked?: any;
filePrefix?: any;
} }

View File

@@ -18,7 +18,9 @@ request.interceptors.response.use(
return Promise.reject(error); return Promise.reject(error);
} }
const { status } = errorResponse; const { status } = errorResponse;
if (status !== 200) { if (status === 400) {
Toast.error(errorResponse.data.detail);
} else if (status !== 200) {
Toast.error("status: " + status); Toast.error("status: " + status);
} }
return Promise.reject(error); return Promise.reject(error);

View File

@@ -14,6 +14,7 @@ import {
VTag, VTag,
} from "@halo-dev/components"; } from "@halo-dev/components";
import CarbonFolderDetailsReference from "~icons/carbon/folder-details-reference"; import CarbonFolderDetailsReference from "~icons/carbon/folder-details-reference";
import IconErrorWarning from "~icons/ri/error-warning-line";
import {computed, onMounted, ref, watch} from "vue"; import {computed, onMounted, ref, watch} from "vue";
import { import {
getApisS3OsHaloRunV1Alpha1ObjectsByPolicyName, getApisS3OsHaloRunV1Alpha1ObjectsByPolicyName,
@@ -27,10 +28,14 @@ const policyName = ref<string>("");
const page = ref(1); const page = ref(1);
const size = ref(50); const size = ref(50);
const policyOptions = ref<{ label: string; value: string; attrs: any }[]>([{ const policyOptions = ref<{ label: string; value: string; attrs: any }[]>([{
label: "请选择策略", label: "请选择存储策略",
value: "", value: "",
attrs: {disabled: true} attrs: {disabled: true}
}]); }]);
// update when fetch first page
const filePrefix = ref<string>("");
// update when user input
const filePrefixBind = ref<string>("");
const s3Objects = ref<S3ListResult>({ const s3Objects = ref<S3ListResult>({
objects: [], objects: [],
hasMore: false, hasMore: false,
@@ -58,15 +63,15 @@ const selectedLinkedStatusItem = ref<boolean | undefined>(linkedStatusItems[0].v
const emptyTips = computed(() => { const emptyTips = computed(() => {
if (isFetchingPolicies.value) { if (isFetchingPolicies.value) {
return "正在加载策略"; return "正在加载存储策略";
} else { } else {
if (policyOptions.value.length <= 1) { if (policyOptions.value.length <= 1) {
return "没有可用的策略请前往【附件】添加S3策略"; return "没有可用的存储策略请前往【附件】添加S3存储策略";
} else { } else {
if (!policyName.value) { if (!policyName.value) {
return "请在左上方选择策略"; return "请在左上方选择存储策略";
} else { } else {
return "该策略的 桶/文件夹 下没有文件"; return "该存储策略的 桶/文件夹 下没有文件";
} }
} }
} }
@@ -92,7 +97,7 @@ const fetchPolicies = async () => {
const policiesData = await getApisS3OsHaloRunV1Alpha1PoliciesS3(); const policiesData = await getApisS3OsHaloRunV1Alpha1PoliciesS3();
if (policiesData.status == 200) { if (policiesData.status == 200) {
policyOptions.value = [{ policyOptions.value = [{
label: "请选择策略", label: "请选择存储策略",
value: "", value: "",
attrs: {disabled: true} attrs: {disabled: true}
}]; }];
@@ -139,6 +144,8 @@ const clearTokenAndObject = () => {
s3Objects.value.nextContinuationObject = ""; s3Objects.value.nextContinuationObject = "";
}; };
// filePrefix will not be updated from user input
// if you want to update filePrefix, please call `handleFirstPage`
const fetchObjects = async () => { const fetchObjects = async () => {
if (!policyName.value) { if (!policyName.value) {
return; return;
@@ -152,6 +159,7 @@ const fetchObjects = async () => {
continuationToken: s3Objects.value.currentToken, continuationToken: s3Objects.value.currentToken,
continuationObject: s3Objects.value.currentContinuationObject, continuationObject: s3Objects.value.currentContinuationObject,
unlinked: selectedLinkedStatusItem.value, unlinked: selectedLinkedStatusItem.value,
filePrefix: filePrefix.value
}); });
if (objectsData.status == 200) { if (objectsData.status == 200) {
s3Objects.value = objectsData.data; s3Objects.value = objectsData.data;
@@ -222,6 +230,7 @@ const handleFirstPage = () => {
isFetching.value = true; isFetching.value = true;
page.value = 1; page.value = 1;
clearTokenAndObject(); clearTokenAndObject();
filePrefix.value = filePrefixBind.value;
fetchObjects(); fetchObjects();
}; };
@@ -255,18 +264,26 @@ const handleModalClose = () => {
<div class="flex w-full flex-1 items-center sm:w-auto"> <div class="flex w-full flex-1 items-center sm:w-auto">
<div <div
v-if="!selectedFiles.length" v-if="!selectedFiles.length"
class="flex items-center gap-2" class="flex flex-wrap items-center gap-2"
> >
策略: <span class="whitespace-nowrap">存储策略:</span>
<FormKit <FormKit
id="policyChoose" id="policyChoose"
outer-class="!p-0 w-48" outer-class="!p-0"
style="min-width: 10rem;"
v-model="policyName" v-model="policyName"
name="policyName" name="policyName"
type="select" type="select"
:options="policyOptions" :options="policyOptions"
@change="fetchObjects()" @change="handleFirstPage"
></FormKit> ></FormKit>
<icon-error-warning v-if="!policyName" class="text-red-500"/>
<SearchInput
v-model="filePrefixBind"
v-if="policyName"
placeholder="请输入文件名前缀搜索"
@update:modelValue="handleFirstPage"
></SearchInput>
</div> </div>
<VSpace v-else> <VSpace v-else>
<VButton type="primary" @click="handleLink"> <VButton type="primary" @click="handleLink">

View File

@@ -28,13 +28,13 @@ public class S3LinkController {
@RequestParam(name = "continuationToken", required = false) String continuationToken, @RequestParam(name = "continuationToken", required = false) String continuationToken,
@RequestParam(name = "continuationObject", required = false) String continuationObject, @RequestParam(name = "continuationObject", required = false) String continuationObject,
@RequestParam(name = "pageSize") Integer pageSize, @RequestParam(name = "pageSize") Integer pageSize,
@RequestParam(name = "unlinked", required = false, defaultValue = "false") @RequestParam(name = "unlinked", required = false, defaultValue = "false") Boolean unlinked,
Boolean unlinked) { @RequestParam(name = "filePrefix", required = false) String filePrefix) {
if (unlinked) { if (unlinked) {
return s3LinkService.listObjectsUnlinked(policyName, continuationToken, return s3LinkService.listObjectsUnlinked(policyName, continuationToken,
continuationObject, pageSize); continuationObject, pageSize, filePrefix);
} else { } else {
return s3LinkService.listObjects(policyName, continuationToken, pageSize); return s3LinkService.listObjects(policyName, continuationToken, pageSize, filePrefix);
} }
} }

View File

@@ -10,10 +10,10 @@ public interface S3LinkService {
Flux<Policy> listS3Policies(); Flux<Policy> listS3Policies();
Mono<S3ListResult> listObjects(String policyName, String continuationToken, Mono<S3ListResult> listObjects(String policyName, String continuationToken,
Integer pageSize); Integer pageSize, String filePrefix);
Mono<LinkResult> addAttachmentRecords(String policyName, Set<String> objectKeys); 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, String filePrefix);
} }

View File

@@ -3,6 +3,7 @@ package run.halo.s3os;
import static run.halo.s3os.S3OsAttachmentHandler.OBJECT_KEY; import static run.halo.s3os.S3OsAttachmentHandler.OBJECT_KEY;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
@@ -14,6 +15,7 @@ import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.Sort;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.ReactiveSecurityContextHolder; import org.springframework.security.core.context.ReactiveSecurityContextHolder;
@@ -53,12 +55,12 @@ public class S3LinkServiceImpl implements S3LinkService {
@Override @Override
public Flux<Policy> listS3Policies() { public Flux<Policy> listS3Policies() {
return client.list(Policy.class, (policy) -> "s3os".equals( return client.list(Policy.class, (policy) -> "s3os".equals(
policy.getSpec().getTemplateName()), null); policy.getSpec().getTemplateName()), Comparator.naturalOrder());
} }
@Override @Override
public Mono<S3ListResult> listObjects(String policyName, String continuationToken, public Mono<S3ListResult> listObjects(String policyName, String continuationToken,
Integer pageSize) { Integer pageSize, String filePrefix) {
return client.fetch(Policy.class, policyName) return client.fetch(Policy.class, policyName)
.flatMap((policy) -> { .flatMap((policy) -> {
var configMapName = policy.getSpec().getConfigMapName(); var configMapName = policy.getSpec().getConfigMapName();
@@ -68,11 +70,11 @@ public class S3LinkServiceImpl implements S3LinkService {
var properties = handler.getProperties(configMap); var properties = handler.getProperties(configMap);
var finalLocation = FilePathUtils.getFilePathByPlaceholder(properties.getLocation()); var finalLocation = FilePathUtils.getFilePathByPlaceholder(properties.getLocation());
return Mono.using(() -> handler.buildS3Client(properties), return Mono.using(() -> handler.buildS3Client(properties),
// 执行 listObjects
(s3Client) -> Mono.fromCallable( (s3Client) -> Mono.fromCallable(
() -> s3Client.listObjectsV2(ListObjectsV2Request.builder() () -> s3Client.listObjectsV2(ListObjectsV2Request.builder()
.bucket(properties.getBucket()) .bucket(properties.getBucket())
.prefix(StringUtils.isNotEmpty(finalLocation) .prefix(buildPrefix(finalLocation, filePrefix))
? finalLocation + "/" : null)
.delimiter("/") .delimiter("/")
.maxKeys(pageSize) .maxKeys(pageSize)
.continuationToken(StringUtils.isNotEmpty(continuationToken) .continuationToken(StringUtils.isNotEmpty(continuationToken)
@@ -81,14 +83,16 @@ public class S3LinkServiceImpl implements S3LinkService {
S3Client::close) S3Client::close)
.flatMap(listObjectsV2Response -> { .flatMap(listObjectsV2Response -> {
List<S3Object> contents = listObjectsV2Response.contents(); List<S3Object> contents = listObjectsV2Response.contents();
// 过滤掉目录并转换为ObjectVo
var objectVos = contents var objectVos = contents
.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));
// 获取已经关联的附件并标记
ListOptions listOptions = new ListOptions(); ListOptions listOptions = new ListOptions();
listOptions.setFieldSelector( listOptions.setFieldSelector(
FieldSelector.of(QueryFactory.equal("spec.policyName", policyName))); FieldSelector.of(QueryFactory.equal("spec.policyName", policyName)));
return client.listAll(Attachment.class, listOptions, null) return client.listAll(Attachment.class, listOptions, Sort.unsorted())
.doOnNext(attachment -> { .doOnNext(attachment -> {
S3ListResult.ObjectVo objectVo = S3ListResult.ObjectVo objectVo =
objectVos.get(attachment.getMetadata().getAnnotations() objectVos.get(attachment.getMetadata().getAnnotations()
@@ -130,7 +134,7 @@ public class S3LinkServiceImpl implements S3LinkService {
ListOptions listOptions = new ListOptions(); ListOptions listOptions = new ListOptions();
listOptions.setFieldSelector( listOptions.setFieldSelector(
FieldSelector.of(QueryFactory.equal("spec.policyName", policyName))); FieldSelector.of(QueryFactory.equal("spec.policyName", policyName)));
return client.listAll(Attachment.class, listOptions, null) return client.listAll(Attachment.class, listOptions, Sort.unsorted())
.filter(attachment -> StringUtils.isNotBlank( .filter(attachment -> StringUtils.isNotBlank(
MetadataUtil.nullSafeAnnotations(attachment).get(S3OsAttachmentHandler.OBJECT_KEY)) MetadataUtil.nullSafeAnnotations(attachment).get(S3OsAttachmentHandler.OBJECT_KEY))
&& objectKeys.contains( && objectKeys.contains(
@@ -163,7 +167,7 @@ public class S3LinkServiceImpl implements S3LinkService {
@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, String filePrefix) {
// TODO 优化成查一次数据库 // TODO 优化成查一次数据库
return Mono.defer(() -> { return Mono.defer(() -> {
List<S3ListResult.ObjectVo> s3Objects = new ArrayList<>(); List<S3ListResult.ObjectVo> s3Objects = new ArrayList<>();
@@ -172,7 +176,8 @@ public class S3LinkServiceImpl implements S3LinkService {
return Flux.defer(() -> Flux.just( return Flux.defer(() -> Flux.just(
new TokenState(null, currToken.get() == null ? "" : currToken.get()))) new TokenState(null, currToken.get() == null ? "" : currToken.get())))
.flatMap(tokenState -> listObjects(policyName, tokenState.nextToken, pageSize)) .flatMap(tokenState -> listObjects(policyName, tokenState.nextToken,
pageSize, filePrefix))
.flatMap(s3ListResult -> { .flatMap(s3ListResult -> {
var filteredObjects = s3ListResult.getObjects(); var filteredObjects = s3ListResult.getObjects();
if (!continuationObjectMatched.get()) { if (!continuationObjectMatched.get()) {
@@ -267,4 +272,18 @@ public class S3LinkServiceImpl implements S3LinkService {
.flatMap(func); .flatMap(func);
} }
String buildPrefix(String finalLocation, String filePrefix) {
if (StringUtils.isBlank(finalLocation) && StringUtils.isBlank(filePrefix)) {
return null;
}
StringBuilder sb = new StringBuilder();
if (StringUtils.isNotBlank(finalLocation)) {
sb.append(finalLocation).append("/");
}
if (StringUtils.isNotBlank(filePrefix)) {
sb.append(filePrefix);
}
return sb.toString();
}
} }

View File

@@ -1,8 +1,8 @@
package run.halo.s3os; package run.halo.s3os;
import org.pf4j.PluginWrapper;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import run.halo.app.plugin.BasePlugin; import run.halo.app.plugin.BasePlugin;
import run.halo.app.plugin.PluginContext;
/** /**
* @author johnniang * @author johnniang
@@ -11,8 +11,8 @@ import run.halo.app.plugin.BasePlugin;
@Component @Component
public class S3OsPlugin extends BasePlugin { public class S3OsPlugin extends BasePlugin {
public S3OsPlugin(PluginWrapper wrapper) { public S3OsPlugin(PluginContext pluginContext) {
super(wrapper); super(pluginContext);
} }
@Override @Override

View File

@@ -46,9 +46,10 @@ metadata:
name: role-template-s3os-policy-config-validation name: role-template-s3os-policy-config-validation
labels: labels:
halo.run/role-template: "true" halo.run/role-template: "true"
halo.run/hidden: "true"
rbac.authorization.halo.run/aggregate-to-role-template-manage-configmaps: "true" rbac.authorization.halo.run/aggregate-to-role-template-manage-configmaps: "true"
rules: rules:
- apiGroups: ["s3os.halo.run"] - apiGroups: ["s3os.halo.run"]
resources: ["policies/validation"] resources: ["policies/validation"]
resourceNames: ["s3"] resourceNames: ["s3"]
verbs: [ "create" ] verbs: [ "create" ]

View File

@@ -7,8 +7,5 @@ spec:
- group: basic - group: basic
label: 使用提示 label: 使用提示
formSchema: formSchema:
- $formkit: text - $el: p
help: 请前往 “附件 - 存储策略” 添加策略 children: 请前往 “附件 - 存储策略” 添加策略
label: 此处不用设置,请前往 “附件 - 存储策略” 添加策略
name: text
placeholder: 此处不用设置,请前往 “附件 - 存储策略” 添加策略

View File

@@ -6,12 +6,14 @@ spec:
enabled: true enabled: true
requires: ">=2.14.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: