Free FS LogoFree FS
服务端

存储插件

详细介绍如何配置和开发 Free FS 的存储插件。

架构概述

Free FS 采用插件化存储架构,支持通过 SPI(Service Provider Interface)机制动态加载存储平台插件。系统默认实现了三个存储平台(阿里云OSS、RustFS、七牛云),这些平台信息存储在数据库中,而具体的插件实现通过 SPI 机制加载。

核心组件

  • IStorageOperationService: 存储操作接口,定义了所有存储平台必须实现的方法
  • StoragePluginRegistry: 插件注册器,负责通过 SPI 加载和注册插件
  • StorageInstanceFactory: 存储实例工厂,根据配置创建存储实例
  • StorageInstanceCache: 存储实例缓存,管理实例的生命周期
  • StorageConfig: 存储配置类,封装平台配置信息

插件加载流程

  1. 系统启动时,StoragePluginRegistry 通过 Java SPI 机制扫描并加载所有插件
  2. 插件以原型实例(Prototype)的形式注册到注册表中
  3. 当需要创建存储实例时,通过 StorageInstanceFactory 调用原型的 createConfiguredInstance 方法
  4. 创建的实例会被缓存到 StorageInstanceCache 中,避免重复创建

如何快速创建一个存储插件?

方式一:基于 S3 兼容存储(推荐)

如果你的存储平台兼容 S3 API(如 MinIO、腾讯云 COS、AWS S3 等),可以继承 AbstractS3CompatibleStorageService,只需少量代码即可完成实现。

方式二:完全自定义实现

如果存储平台有独特的 API,可以继承 AbstractStorageOperationService,实现所有接口方法。

实现步骤

步骤 1:创建配置类

首先,创建一个配置类来定义你的存储平台需要哪些配置项。

S3 兼容存储示例:

package com.xddcodec.fs.storage.plugin.yourplatform.config;

import com.xddcodec.fs.storage.plugin.core.s3.S3CompatibleConfig;
import lombok.Data;
import lombok.EqualsAndHashCode;
import software.amazon.awssdk.regions.Region;

@Data
@EqualsAndHashCode(callSuper = true)
public class YourPlatformConfig extends S3CompatibleConfig {
    
    public YourPlatformConfig() {
        super();
        // 设置默认值
        setPathStyleAccess(true);  // MinIO 需要设置为 true
        setRegion(Region.US_EAST_1);
    }
    
    // 如果需要额外的配置项,可以在这里添加
    // private String customField;
}

完全自定义示例:

package com.xddcodec.fs.storage.plugin.yourplatform.config;

import lombok.Data;

@Data
public class YourPlatformConfig {
    private String endpoint;
    private String accessKey;
    private String secretKey;
    private String bucket;
    // 其他自定义配置项...
}

步骤 2:实现存储服务类

S3 兼容存储实现:

package com.xddcodec.fs.storage.plugin.yourplatform;

import com.xddcodec.fs.storage.plugin.core.config.StorageConfig;
import com.xddcodec.fs.storage.plugin.core.s3.AbstractS3CompatibleStorageService;
import com.xddcodec.fs.storage.plugin.yourplatform.config.YourPlatformConfig;

public class YourPlatformStorageServiceImpl 
        extends AbstractS3CompatibleStorageService<YourPlatformConfig> {

    // 原型构造函数(SPI 加载用)
    public YourPlatformStorageServiceImpl() {
        super();
    }

    // 配置化构造函数
    public YourPlatformStorageServiceImpl(StorageConfig config) {
        super(config);
    }

    @Override
    public String getPlatformIdentifier() {
        return "your_platform";  // 返回你的平台标识符
    }

    @Override
    protected Class<YourPlatformConfig> getS3ConfigClass() {
        return YourPlatformConfig.class;
    }

    // 可选:自定义配置校验
    @Override
    protected void customValidateConfig(YourPlatformConfig config) {
        // 添加额外的配置校验逻辑
    }

    // 可选:自定义 S3 客户端配置
    @Override
    protected void customizeS3Configuration(
            software.amazon.awssdk.services.s3.S3Configuration.Builder builder) {
        // 自定义 S3 客户端配置
    }
}

完全自定义实现:

package com.xddcodec.fs.storage.plugin.yourplatform;

import com.xddcodec.fs.framework.common.exception.StorageConfigException;
import com.xddcodec.fs.framework.common.exception.StorageOperationException;
import com.xddcodec.fs.storage.plugin.core.AbstractStorageOperationService;
import com.xddcodec.fs.storage.plugin.core.config.StorageConfig;
import com.xddcodec.fs.storage.plugin.yourplatform.config.YourPlatformConfig;
import lombok.extern.slf4j.Slf4j;

import java.io.InputStream;

@Slf4j
public class YourPlatformStorageServiceImpl 
        extends AbstractStorageOperationService {

    private YourPlatformClient client;  // 你的存储平台客户端
    private String bucketName;

    // 原型构造函数
    public YourPlatformStorageServiceImpl() {
        super();
    }

    // 配置化构造函数
    public YourPlatformStorageServiceImpl(StorageConfig config) {
        super(config);
    }

    @Override
    public String getPlatformIdentifier() {
        return "your_platform";
    }

    @Override
    protected void validateConfig(StorageConfig config) {
        try {
            YourPlatformConfig platformConfig = config.toObject(YourPlatformConfig.class);
            
            if (platformConfig == null) {
                throw new StorageConfigException("配置转换失败");
            }
            
            // 验证必填字段
            if (platformConfig.getEndpoint() == null || 
                platformConfig.getEndpoint().trim().isEmpty()) {
                throw new StorageConfigException("endpoint 不能为空");
            }
            // 其他字段验证...
            
        } catch (Exception e) {
            log.error("配置验证失败: {}", e.getMessage(), e);
            throw new StorageConfigException("存储平台配置错误:" + e.getMessage());
        }
    }

    @Override
    protected void initialize(StorageConfig config) {
        try {
            YourPlatformConfig platformConfig = config.toObject(YourPlatformConfig.class);
            
            // 初始化客户端
            this.client = new YourPlatformClient(
                platformConfig.getEndpoint(),
                platformConfig.getAccessKey(),
                platformConfig.getSecretKey()
            );
            this.bucketName = platformConfig.getBucket();
            
            log.info("{} 客户端初始化成功: endpoint={}, bucket={}",
                    getLogPrefix(), platformConfig.getEndpoint(), this.bucketName);
                    
        } catch (Exception e) {
            log.error("客户端初始化失败: {}", e.getMessage(), e);
            throw new StorageConfigException("存储平台配置错误:客户端初始化失败");
        }
    }

    @Override
    public void uploadFile(InputStream inputStream, String objectKey) {
        ensureNotPrototype();
        try {
            // 实现上传逻辑
            client.upload(bucketName, objectKey, inputStream);
            log.debug("{} 文件上传成功: objectKey={}", getLogPrefix(), objectKey);
        } catch (Exception e) {
            log.error("{} 文件上传失败: objectKey={}", getLogPrefix(), objectKey, e);
            throw new StorageOperationException("文件上传失败: " + e.getMessage(), e);
        }
    }

    @Override
    public InputStream downloadFile(String objectKey) {
        ensureNotPrototype();
        try {
            // 实现下载逻辑
            InputStream stream = client.download(bucketName, objectKey);
            log.debug("{} 文件下载成功: objectKey={}", getLogPrefix(), objectKey);
            return stream;
        } catch (Exception e) {
            log.error("{} 文件下载失败: objectKey={}", getLogPrefix(), objectKey, e);
            throw new StorageOperationException("文件下载失败: " + e.getMessage(), e);
        }
    }

    @Override
    public void deleteFile(String objectKey) {
        ensureNotPrototype();
        try {
            // 实现删除逻辑
            client.delete(bucketName, objectKey);
            log.debug("{} 文件删除成功: objectKey={}", getLogPrefix(), objectKey);
        } catch (Exception e) {
            log.error("{} 文件删除失败: objectKey={}", getLogPrefix(), objectKey, e);
            throw new StorageOperationException("文件删除失败: " + e.getMessage(), e);
        }
    }

    @Override
    public void rename(String objectKey, String newFileName) {
        ensureNotPrototype();
        try {
            // 实现重命名逻辑(通常是复制+删除)
            String newKey = objectKey.substring(0, objectKey.lastIndexOf('/') + 1) + newFileName;
            client.copy(bucketName, objectKey, newKey);
            client.delete(bucketName, objectKey);
            log.debug("{} 文件重命名成功: {} -> {}", getLogPrefix(), objectKey, newKey);
        } catch (Exception e) {
            log.error("{} 文件重命名失败: objectKey={}", getLogPrefix(), objectKey, e);
            throw new StorageOperationException("文件重命名失败: " + e.getMessage(), e);
        }
    }

    @Override
    public String getFileUrl(String objectKey, Integer expireSeconds) {
        ensureNotPrototype();
        try {
            // 实现 URL 生成逻辑
            return client.generateUrl(bucketName, objectKey, expireSeconds);
        } catch (Exception e) {
            log.error("{} 生成文件URL失败: objectKey={}", getLogPrefix(), objectKey, e);
            throw new StorageOperationException("生成文件URL失败: " + e.getMessage(), e);
        }
    }

    @Override
    public InputStream getFileStream(String objectKey) {
        return downloadFile(objectKey);
    }

    @Override
    public boolean isFileExist(String objectKey) {
        ensureNotPrototype();
        try {
            return client.exists(bucketName, objectKey);
        } catch (Exception e) {
            log.error("{} 检查文件存在失败: objectKey={}", getLogPrefix(), objectKey, e);
            throw new StorageOperationException("检查文件存在失败: " + e.getMessage(), e);
        }
    }

    // 实现分片上传相关方法(如果支持)
    @Override
    public String initiateMultipartUpload(String objectKey, String mimeType) {
        ensureNotPrototype();
        try {
            // 实现分片上传初始化
            String uploadId = client.initiateMultipartUpload(bucketName, objectKey, mimeType);
            log.debug("{} 初始化分片上传成功: objectKey={}, uploadId={}", 
                    getLogPrefix(), objectKey, uploadId);
            return uploadId;
        } catch (Exception e) {
            log.error("{} 初始化分片上传失败: objectKey={}", getLogPrefix(), objectKey, e);
            throw new StorageOperationException("初始化分片上传失败: " + e.getMessage(), e);
        }
    }

    @Override
    public String uploadPart(String objectKey, String uploadId, int partNumber,
                             long partSize, InputStream partInputStream) {
        ensureNotPrototype();
        try {
            // 实现分片上传
            String eTag = client.uploadPart(bucketName, objectKey, uploadId, 
                    partNumber, partSize, partInputStream);
            log.debug("{} 分片上传成功: objectKey={}, partNumber={}, eTag={}",
                    getLogPrefix(), objectKey, partNumber, eTag);
            return eTag;
        } catch (Exception e) {
            log.error("{} 分片上传失败: objectKey={}, partNumber={}", 
                    getLogPrefix(), objectKey, partNumber, e);
            throw new StorageOperationException("分片上传失败: " + e.getMessage(), e);
        }
    }

    @Override
    public java.util.Set<Integer> listParts(String objectKey, String uploadId) {
        ensureNotPrototype();
        try {
            // 实现列举分片
            return client.listParts(bucketName, objectKey, uploadId);
        } catch (Exception e) {
            log.warn("{} 列举分片失败,返回空集合: {}", getLogPrefix(), e.getMessage());
            return java.util.Collections.emptySet();
        }
    }

    @Override
    public void completeMultipartUpload(String objectKey, String uploadId,
                                       java.util.List<java.util.Map<String, Object>> partETags) {
        ensureNotPrototype();
        try {
            // 实现完成分片上传
            client.completeMultipartUpload(bucketName, objectKey, uploadId, partETags);
            log.info("{} 分片合并成功: objectKey={}, uploadId={}", 
                    getLogPrefix(), objectKey, uploadId);
        } catch (Exception e) {
            log.error("{} 分片合并失败: objectKey={}, uploadId={}", 
                    getLogPrefix(), objectKey, uploadId, e);
            throw new StorageOperationException("分片合并失败: " + e.getMessage(), e);
        }
    }

    @Override
    public void abortMultipartUpload(String objectKey, String uploadId) {
        ensureNotPrototype();
        try {
            // 实现取消分片上传
            client.abortMultipartUpload(bucketName, objectKey, uploadId);
            log.info("{} 分片上传已取消: objectKey={}, uploadId={}", 
                    getLogPrefix(), objectKey, uploadId);
        } catch (Exception e) {
            log.error("{} 取消分片上传失败: objectKey={}, uploadId={}", 
                    getLogPrefix(), objectKey, uploadId, e);
            throw new StorageOperationException("取消分片上传失败: " + e.getMessage(), e);
        }
    }

    @Override
    public void close() {
        if (client != null) {
            client.close();
        }
    }
}

步骤 3:配置 SPI

src/main/resources/META-INF/services/ 目录下创建文件:

文件名: com.xddcodec.fs.storage.plugin.core.IStorageOperationService

文件内容:

com.xddcodec.fs.storage.plugin.yourplatform.YourPlatformStorageServiceImpl

注意: 文件名必须是接口的完整类名,文件内容是实现类的完整类名,每行一个。

步骤 4:配置数据库

storage_platform 表中添加你的存储平台信息:

INSERT INTO storage_platform (name, identifier, config_scheme, icon, link, is_default, `desc`)
VALUES (
    '你的平台名称',
    'your_platform',  -- 必须与 getPlatformIdentifier() 返回值一致
    '[{"label": "Endpoint", "dataType": "string", "identifier": "endpoint", "validation": {"required": true}}, {"label": "Access-Key", "dataType": "string", "identifier": "accessKey", "validation": {"required": true}}, {"label": "Secret-key", "dataType": "string", "identifier": "secretKey", "validation": {"required": true}}, {"label": "Bucket", "dataType": "string", "identifier": "bucket", "validation": {"required": true}}]',
    'icon-your-platform',
    'https://your-platform.com',
    0,
    '你的平台描述'
);

config_scheme 字段说明:

config_scheme 是一个 JSON 数组,用于定义前端动态表单。每个配置项包含以下字段:

  • label: 显示标签
  • dataType: 数据类型(string、number、boolean 等)
  • identifier: 配置项标识符(对应配置类中的字段名)
  • validation: 验证规则
    • required: 是否必填
    • 其他验证规则...

示例:

[
  {
    "label": "Endpoint",
    "dataType": "string",
    "identifier": "endpoint",
    "validation": {
      "required": true
    }
  },
  {
    "label": "Access-Key",
    "dataType": "string",
    "identifier": "accessKey",
    "validation": {
      "required": true
    }
  },
  {
    "label": "Secret-key",
    "dataType": "string",
    "identifier": "secretKey",
    "validation": {
      "required": true
    }
  },
  {
    "label": "Bucket",
    "dataType": "string",
    "identifier": "bucket",
    "validation": {
      "required": true
    }
  }
]

步骤 5:添加依赖

在你的插件项目中添加必要的依赖:

Maven 依赖(S3 兼容存储):

<dependencies>
    <!-- Free FS 存储插件核心 -->
    <dependency>
        <groupId>com.xddcodec.fs</groupId>
        <artifactId>fs-storage-plugin-core</artifactId>
        <version>${fs.version}</version>
    </dependency>
    
    <!-- AWS S3 SDK(S3 兼容存储需要) -->
    <dependency>
        <groupId>software.amazon.awssdk</groupId>
        <artifactId>s3</artifactId>
        <version>2.20.0</version>
    </dependency>
</dependencies>

步骤 6:集成到主项目

插件模块开发完成后,需要将其集成到主项目中,以便系统能够加载和使用该插件。

6.1 在 fs-dependencies 中配置版本

fs-dependencies 模块的 pom.xml 中添加插件的版本管理:

<dependencyManagement>
    <dependencies>
        <!-- 其他依赖管理... -->
        
        <!-- 你的存储插件 -->
        <dependency>
            <groupId>com.xddcodec.fs</groupId>
            <artifactId>storage-plugin-yourplatform</artifactId>
            <version>${revision}</version>
        </dependency>
    </dependencies>
</dependencyManagement>

说明fs-dependencies 模块用于统一管理所有模块的版本,确保版本一致性。

6.2 在 storage-plugin-boot 中添加依赖

storage-plugin-boot 模块的 pom.xml 中添加你的插件依赖:

<dependencies>
    <!-- 存储插件核心 -->
    <dependency>
        <groupId>com.xddcodec.fs</groupId>
        <artifactId>storage-plugin-core</artifactId>
    </dependency>
    
    <!-- 本地存储插件 -->
    <dependency>
        <groupId>com.xddcodec.fs</groupId>
        <artifactId>storage-plugin-local</artifactId>
    </dependency>
    
    <!-- 阿里云OSS插件(可选,根据需要添加) -->
    <dependency>
        <groupId>com.xddcodec.fs</groupId>
        <artifactId>storage-plugin-aliyunoss</artifactId>
    </dependency>
    
    <!-- RustFS插件(可选,根据需要添加) -->
    <dependency>
        <groupId>com.xddcodec.fs</groupId>
        <artifactId>storage-plugin-rustfs</artifactId>
    </dependency>
    
    <!-- 你的存储插件(可选,根据需要添加) -->
    <dependency>
        <groupId>com.xddcodec.fs</groupId>
        <artifactId>storage-plugin-yourplatform</artifactId>
    </dependency>
</dependencies>

重要提示

  • 插件依赖是可选的,可以根据实际需要添加或移除
  • 如果不添加插件依赖,即使插件已开发完成,系统也无法加载该插件
  • 添加依赖后,需要重新编译和打包项目

6.3 验证集成

完成上述配置后:

  1. 重新编译项目

    mvn clean install
  2. 检查依赖:确认插件 JAR 文件已包含在 storage-plugin-boot 模块的 classpath 中。

  3. 启动应用:启动应用后,检查日志确认插件已加载:

    开始加载存储插件...
    注册存储插件: your_platform
    存储插件加载完成,共加载 X 个插件: [...]

实现示例

示例 1:RustFS(S3 兼容)

RustFS 是一个基于 S3 兼容协议的存储平台,实现非常简单:

package com.xddcodec.fs.storage.plugin.rustfs;

import com.xddcodec.fs.framework.common.enums.StoragePlatformIdentifierEnum;
import com.xddcodec.fs.storage.plugin.core.config.StorageConfig;
import com.xddcodec.fs.storage.plugin.core.s3.AbstractS3CompatibleStorageService;
import com.xddcodec.fs.storage.plugin.rustfs.config.RustFsConfig;

public class RustFsStorageServiceImpl 
        extends AbstractS3CompatibleStorageService<RustFsConfig> {

    public RustFsStorageServiceImpl() {
        super();
    }

    public RustFsStorageServiceImpl(StorageConfig config) {
        super(config);
    }

    @Override
    public String getPlatformIdentifier() {
        return StoragePlatformIdentifierEnum.RUSTFS.getIdentifier();
    }

    @Override
    protected Class<RustFsConfig> getS3ConfigClass() {
        return RustFsConfig.class;
    }
}

示例 2:阿里云 OSS(完全自定义)

阿里云 OSS 使用自己的 SDK,需要完全自定义实现,可以参考 AliyunOssStorageServiceImpl 的实现。

配置 Schema(可选)

接口中提供了 getConfigSchema() 方法,用于返回配置的 JSON Schema。目前系统主要从数据库的 config_scheme 字段读取配置 Schema,但如果你需要动态生成 Schema,可以重写此方法:

@Override
public String getConfigSchema() {
    // 返回 JSON Schema 字符串
    return "[{\"label\": \"Endpoint\", \"dataType\": \"string\", \"identifier\": \"endpoint\", \"validation\": {\"required\": true}}]";
}

注意:目前系统优先使用数据库中的 config_scheme 字段,此方法主要用于特殊场景。

注意事项

  1. 平台标识符唯一性getPlatformIdentifier() 返回的标识符必须在整个系统中唯一,且必须与数据库中的 identifier 字段一致。

  2. 原型实例:插件类必须提供无参构造函数(原型构造函数),用于 SPI 加载。原型实例只用于获取 Schema 和创建新实例,不能执行实际的存储操作。

  3. 配置化实例:通过 createConfiguredInstance 方法创建的实例才是真正用于业务操作的实例。

  4. 资源管理:如果插件使用了需要关闭的资源(如 HTTP 客户端、连接池等),必须在 close() 方法中正确释放。

  5. 异常处理:所有方法都应该抛出 StorageOperationExceptionStorageConfigException,以便系统统一处理。

  6. 日志记录:使用 getLogPrefix() 方法获取日志前缀,便于追踪问题。

  7. 线程安全:存储实例可能被多个线程并发访问,确保实现是线程安全的。

测试插件

完成插件开发和集成后,按以下步骤测试:

  1. 编译项目:在主项目根目录执行编译命令:

    mvn clean install

    确保插件模块编译成功,并且已正确集成到 storage-plugin-boot 模块中。

  2. 验证依赖:检查 storage-plugin-boot 模块的依赖中是否包含你的插件:

    mvn dependency:tree -pl storage-plugin-boot | grep storage-plugin-yourplatform
  3. 启动应用:启动应用后,检查日志中是否有插件加载信息:

    开始加载存储插件...
    注册存储插件: your_platform
    存储插件加载完成,共加载 X 个插件: [...]

    如果插件未出现在日志中,请检查:

    • fs-dependencies 中是否配置了版本管理
    • storage-plugin-boot 中是否添加了依赖
    • SPI 配置文件路径和内容是否正确
  4. 验证功能:在管理后台创建存储配置,测试上传、下载等功能。

使用已开发的插件

插件开发完成并完成集成配置后(参考步骤 6:集成到主项目),需要完成以下步骤才能在系统中使用:

步骤 1:验证插件加载

启动应用后,检查日志确认插件已成功加载:

开始加载存储插件...
注册存储插件: your_platform
存储插件加载完成,共加载 X 个插件: [local, aliyun_oss, rustfs, your_platform]

如果插件未出现在日志中,请检查:

  • SPI 配置文件路径和内容是否正确
  • JAR 文件是否在 classpath 中
  • 插件类是否实现了 IStorageOperationService 接口

步骤 2:配置数据库

storage_platform 表中添加你的存储平台信息(如果尚未添加):

INSERT INTO storage_platform (name, identifier, config_scheme, icon, link, is_default, `desc`)
VALUES (
    '你的平台名称',
    'your_platform',  -- 必须与 getPlatformIdentifier() 返回值一致
    '[{"label": "Endpoint", "dataType": "string", "identifier": "endpoint", "validation": {"required": true}}, {"label": "Access-Key", "dataType": "string", "identifier": "accessKey", "validation": {"required": true}}, {"label": "Secret-key", "dataType": "string", "identifier": "secretKey", "validation": {"required": true}}, {"label": "Bucket", "dataType": "string", "identifier": "bucket", "validation": {"required": true}}]',
    'icon-your-platform',
    'https://your-platform.com',
    0,
    '你的平台描述'
);

注意identifier 字段必须与插件中 getPlatformIdentifier() 方法的返回值完全一致。

步骤 3:在页面中添加配置

  1. 访问存储平台管理页面:登录系统后,进入"存储配置"页面。

  2. 添加新配置:点击"添加配置"按钮,打开配置对话框。

  3. 选择存储平台:在下拉菜单中选择你开发的存储平台(如 "你的平台名称")。

选择存储平台

  1. 填写配置信息:根据 config_scheme 中定义的字段,填写相应的配置项:

    • Endpoint:存储服务的端点地址
    • Access-Key:访问密钥
    • Secret-key:密钥
    • Bucket:存储桶名称
    • 其他自定义配置项...
  2. 添加备注(推荐):为配置添加备注,便于识别(如"生产环境"、"测试环境"等)。

  3. 保存配置:点击"保存配置"按钮,系统会验证配置并创建存储实例。

步骤 4:启用配置

配置保存成功后,你可以:

  1. 查看配置列表:在"我的存储配置"区域可以看到新添加的配置卡片,状态显示为"已禁用"。

  2. 启用配置:点击配置卡片上的"启用"按钮(电源图标),系统会:

    • 验证配置的有效性
    • 创建存储实例并缓存
    • 将当前存储平台切换为该配置
  3. 确认切换:启用成功后:

    • 配置卡片状态变为"已启用"
    • 右上角显示当前启用的存储平台
    • 所有文件操作将使用该存储平台

启用配置

步骤 5:切换存储平台

已启用状态

  • 启用其他配置:点击其他配置的"启用"按钮,系统会自动禁用当前配置并启用新配置。
  • 快速切换:点击右上角的存储平台切换按钮,可以快速返回存储配置页面进行切换。

重要提示

  • 同时只能有一个存储配置处于启用状态
  • 如果都不启用,则平台默认使用本地存储插件
  • 切换存储平台不会迁移已有文件,新上传的文件将存储到新平台
  • 平台目前以单独的Bucket作为独立的存储管理单元,也就是说同一个存储平台的配置可以有多个不同的Bucket,平台请求头会传递已启用配置的唯一id
  • 建议在切换前备份重要数据

常见问题

Q: 插件已加载,但在下拉列表中看不到?

A: 检查数据库中的 storage_platform 表,确认:

  • identifier 字段与插件中的 getPlatformIdentifier() 返回值一致
  • 记录已正确插入数据库

Q: 配置保存失败?

A: 检查:

  • 配置项是否填写完整(必填项不能为空)
  • 配置值格式是否正确
  • 网络连接是否正常(如 endpoint 是否可访问)
  • 查看应用日志中的错误信息

Q: 启用配置后无法上传文件?

A: 检查:

  • 存储实例是否创建成功(查看日志)
  • 配置的访问凭证是否正确
  • 存储桶是否存在且有写入权限
  • 网络连接是否正常

目录