Skip to main content

Minio文件存储


Minio文件存储

微服务环境下,我们要尽量将服务进行拆分,让不同的服务只做好自己的事情,例如我们的设计中,包含文件服务,用户服务,聊天服务的等。

这样的好处是:如果有一天我们发现文件的上传下载占用的带宽特别高,那么肯定需要扩容。

如果将文件服务耦合在用户服务或者聊天服务中,那么就相当于要对整个用户服务进行扩容,得不偿失。

而我们将文件服务分开,就可以更好的对文件服务单一的进行扩容操作,更好的利用资源。

Docker运行Minio

  1. 拉取镜像
docker pull minio/minio
  1. 运行容器
docker run -p 9000:9000 -p 9090:9090 \
   --name minio -d --restart=always \
  -e "MINIO_ACCESS_KEY=minio" \
  -e "MINIO_SECRET_KEY=minio123" \
  -v ~/tools/docker-volumes/minio:/data \
  -v ~/tools/docker-volumes/minio-config:/root/.minio \
  minio/minio server /data --console-address ":9090"
  1. 访问:http://localhost:9090/browseropen in new window

新建一个桶

image-20240529192405127

设置为公开的桶

image-20240529192502315

新建一个密钥:

image-20240529192650734

Minio配置

依赖

<dependency>
    <groupId>io.minio</groupId>
    <artifactId>minio</artifactId>
    <version>8.4.5</version>
</dependency>

导入oss配置properties

oss.enabled=true
oss.type=minio
oss.endpoint=http://localhost:9000
oss.access-key=lIJJ2kaz6F4fmCu1IDiu
oss.secret-key=z7tD0vNXmB8LYZkfPag9aQ9QNxVeaQXrDli7Eejm
oss.bucketName=yunfei-chat

OSS属性配置

@Data
@ConfigurationProperties(prefix = "oss")
public class OssProperties {

    /**
     * 是否开启
     */
    Boolean enabled;

    /**
     * 存储对象服务器类型
     */
    OssType type;

    /**
     * OSS 访问端点,集群时需提供统一入口
     */
    String endpoint;

    /**
     * 用户名
     */
    String accessKey;

    /**
     * 密码
     */
    String secretKey;

    /**
     * 存储桶
     */
    String bucketName;
}

Minio功能

@Slf4j
@AllArgsConstructor
public class MinIOTemplate {

    /**
     * MinIO 客户端
     */
    MinioClient minioClient;

    /**
     * MinIO 配置类
     */
    OssProperties ossProperties;

    /**
     * 查询所有存储桶
     *
     * @return Bucket 集合
     */
    @SneakyThrows
    public List<Bucket> listBuckets() {
        return minioClient.listBuckets();
    }

    /**
     * 桶是否存在
     *
     * @param bucketName 桶名
     * @return 是否存在
     */
    @SneakyThrows
    public boolean bucketExists(String bucketName) {
        return minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
    }

    /**
     * 创建存储桶
     *
     * @param bucketName 桶名
     */
    @SneakyThrows
    public void makeBucket(String bucketName) {
        if (!bucketExists(bucketName)) {
            minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
        }
    }

    /**
     * 删除一个空桶 如果存储桶存在对象不为空时,删除会报错。
     *
     * @param bucketName 桶名
     */
    @SneakyThrows
    public void removeBucket(String bucketName) {
        minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());
    }

    /**
     * 返回临时带签名、过期时间一天、PUT请求方式的访问URL
     */
    @SneakyThrows
    public OssResp getPreSignedObjectUrl(OssReq req) {
        String absolutePath = req.isAutoPath() ? generateAutoPath(req) : req.getFilePath() + StrUtil.SLASH + req.getFileName();
        String url = minioClient.getPresignedObjectUrl(
                GetPresignedObjectUrlArgs.builder()
                        .method(Method.PUT)
                        .bucket(ossProperties.getBucketName())
                        .object(absolutePath)
                        .expiry(60 * 60 * 24)
                        .build());
        return OssResp.builder()
                .uploadUrl(url)
                .downloadUrl(getDownloadUrl(ossProperties.getBucketName(), absolutePath))
                .build();
    }

    private String getDownloadUrl(String bucket, String pathFile) {
        return ossProperties.getEndpoint() + StrUtil.SLASH + bucket + pathFile;
    }

    /**
     * GetObject接口用于获取某个文件(Object)。此操作需要对此Object具有读权限。
     *
     * @param bucketName  桶名
     * @param ossFilePath Oss文件路径
     */
    @SneakyThrows
    public InputStream getObject(String bucketName, String ossFilePath) {
        return minioClient.getObject(
                GetObjectArgs.builder().bucket(bucketName).object(ossFilePath).build());
    }

    /**
     * 查询桶的对象信息
     *
     * @param bucketName 桶名
     * @param recursive  是否递归查询
     * @return
     */
    @SneakyThrows
    public Iterable<Result<Item>> listObjects(String bucketName, boolean recursive) {
        return minioClient.listObjects(
                ListObjectsArgs.builder().bucket(bucketName).recursive(recursive).build());
    }

    /**
     * 生成随机文件名,防止重复
     *
     * @return
     */
    public String generateAutoPath(OssReq req) {
        String uid = Optional.ofNullable(req.getUid()).map(String::valueOf).orElse("000000");
        cn.hutool.core.lang.UUID uuid = cn.hutool.core.lang.UUID.fastUUID();
        String suffix = FileNameUtil.getSuffix(req.getFileName());
        String yearAndMonth = DateUtil.format(new Date(), DatePattern.NORM_MONTH_PATTERN);
        return req.getFilePath() + StrUtil.SLASH + yearAndMonth + StrUtil.SLASH + uid + StrUtil.SLASH + uuid + StrUtil.DOT + suffix;
    }

    /**
     * 获取带签名的临时上传元数据对象,前端可获取后,直接上传到Minio
     *
     * @param bucketName
     * @param fileName
     * @return
     */
    @SneakyThrows
    public Map<String, String> getPreSignedPostFormData(String bucketName, String fileName) {
        // 为存储桶创建一个上传策略,过期时间为7天
        PostPolicy policy = new PostPolicy(bucketName, ZonedDateTime.now().plusDays(7));
        // 设置一个参数key,值为上传对象的名称
        policy.addEqualsCondition("key", fileName);
        // 添加Content-Type以"image/"开头,表示只能上传照片
        policy.addStartsWithCondition("Content-Type", "image/");
        // 设置上传文件的大小 64kiB to 10MiB.
        policy.addContentLengthRangeCondition(64 * 1024, 10 * 1024 * 1024);
        return minioClient.getPresignedPostFormData(policy);
    }

}

封装为starter

@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(OssProperties.class)
@ConditionalOnExpression("${oss.enabled}")
@ConditionalOnProperty(value = "oss.type", havingValue = "minio")
public class MinIOConfiguration {


    @Bean
    @SneakyThrows
    @ConditionalOnMissingBean(MinioClient.class)
    public MinioClient minioClient(OssProperties ossProperties) {
        return MinioClient.builder()
                .endpoint(ossProperties.getEndpoint())
                .credentials(ossProperties.getAccessKey(), ossProperties.getSecretKey())
                .build();
    }

    @Bean
    @ConditionalOnBean({MinioClient.class})
    @ConditionalOnMissingBean(MinIOTemplate.class)
    public MinIOTemplate minioTemplate(MinioClient minioClient, OssProperties ossProperties) {
        return new MinIOTemplate(minioClient, ossProperties);
    }
}

在resources目录下的META-INFspring.factories

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.yunfei.chat.oss.minio.MinIOConfiguration

##测试

@Test
public void getUploadUrl() {
    OssReq ossReq = OssReq.builder()
            .fileName("test.jpeg")
            .filePath("/test")
            .autoPath(false)
            .build();
    OssResp preSignedObjectUrl = minIOTemplate.getPreSignedObjectUrl(ossReq);
    System.out.println(preSignedObjectUrl);
}

image-20240529192945998

拿uploadUrl进行上传,这里选body里面的二进制文件

image-20240529193337435

上传成功

image-20240529193358525