RuoYi-Vue-Plus 阅读笔记 – 10 – Minio 文件存储
本文最后更新于 209 天前,如有失效请评论区留言。

MinIO作为一款基于Golang 编程语言开发的一款高性能的分布式式存储方案的开源项目,有十分完善的官方文档。
文档地址:https://docs.min.io/cn/
本节介绍下使用 minio 来作为项目的对象存储,想要使用 minio,首先需要搭建 minio 服务,然后在应用端接入

Minio 服务搭建

1panel 的应用商店可以直接安装 minio,一键部署
如果使用 docker 部署,按照如下步骤:

首先我们要创建两个文件目录:一个用来存放 MinIO 的配置文件,一个用来存储我们上传文件数据。

mkdir -p /home/minio/config
mkdir -p /home/minio/data
  • /home/minio/config 用于存放 MinIO 的配置文件
  • /home/minio/data 用于存储上传的文件数据

接下来我们可以通过如下命令拉取最新版镜像并创建 MinIO 容器运行。如果需要下载指定版本可以点击前往DockerHub仓库选择下载

docker run -p 9000:9000 -p 9001:9001 \
-d --restart=always \
-e "MINIO_ACCESS_KEY=admin" \
-e "MINIO_SECRET_KEY=password" \
-v /home/minio/data:/data \
-v /home/minio/config:/root/.minio \
minio/minio server \
/data \
--console-address ":9000" 

命令行解释:

  • MINIO_ACCESS_KEY 和 MINIO_SECRET_KEY 为UI界面登录账号密码
  • -d  将容器以后台(守护进程)模式运行,并与终端分离。
  • --restart=always 选项指定容器在停止后总是自动重启。
  • /home/minio/data 挂载的存储上传文件的目录
  • /home/minio/config 挂载的配置文件
  • minio/minio server 使用MinIO 镜像并启动
  • /data 要使用的数据目录
  • --console-address 指定UI 界面的端口
  • 9000:9000 映射UI界面端口
  • 9001:9001 映射服务器端口

最后在浏览器中访问 http://服务器IP:9001 即可访问到MinIO的控制台

在控制台界面中,需要在 Access keys 中获取 AccessKey 和 SecretKey,这个一般在项目的配置中会用到。具体的可以参考网上的教程

整合 Minio

依赖

这里使用AWS S3 来操作 Minio,S3 存储协议兼容所有支持S3协议的云厂商,包括主流的阿里云 腾讯云 七牛云

<dependency>  
    <groupId>com.amazonaws</groupId>  
    <artifactId>aws-java-sdk-s3</artifactId>  
</dependency>

OSS 对象存储配置

/**
 * OSS对象存储 配置属性
 */
@Data
public class OssProperties {

    /**
     * 租户id
     */
    private String tenantId;

    /**
     * 访问站点
     */
    private String endpoint;

    /**
     * 自定义域名
     */
    private String domain;

    /**
     * 前缀
     */
    private String prefix;

    /**
     * ACCESS_KEY
     */
    private String accessKey;

    /**
     * SECRET_KEY
     */
    private String secretKey;

    /**
     * 存储空间名
     */
    private String bucketName;

    /**
     * 存储区域
     */
    private String region;

    /**
     * 是否https(Y=是,N=否)
     */
    private String isHttps;

    /**
     * 桶权限类型(0private 1public 2custom)
     */
    private String accessPolicy;

}

OssClient 实例创建

使用 asw-sdk-java 来对Minio Server,首先创建 AmazonS3 客户端对象

public class OssClient {
    private final AmazonS3 client;

    public OssClient(String configKey, OssProperties ossProperties) {
        this.configKey = configKey;
        this.properties = ossProperties;
        try {
            AwsClientBuilder.EndpointConfiguration endpointConfig =
                new AwsClientBuilder.EndpointConfiguration(properties.getEndpoint(), properties.getRegion());

            AWSCredentials credentials = new BasicAWSCredentials(properties.getAccessKey(), properties.getSecretKey());
            AWSCredentialsProvider credentialsProvider = new AWSStaticCredentialsProvider(credentials);
            ClientConfiguration clientConfig = new ClientConfiguration();
            if (OssConstant.IS_HTTPS.equals(properties.getIsHttps())) {
                clientConfig.setProtocol(Protocol.HTTPS);
            } else {
                clientConfig.setProtocol(Protocol.HTTP);
            }
            AmazonS3ClientBuilder build = AmazonS3Client.builder()
                .withEndpointConfiguration(endpointConfig)
                .withClientConfiguration(clientConfig)
                .withCredentials(credentialsProvider)
                .disableChunkedEncoding();
            if (!StringUtils.containsAny(properties.getEndpoint(), OssConstant.CLOUD_SERVICE)) {
                // minio 使用https限制使用域名访问 需要此配置 站点填域名
                build.enablePathStyleAccess();
            }
            this.client = build.build();

            createBucket();
        } catch (Exception e) {
            if (e instanceof OssException) {
                throw e;
            }
            throw new OssException("配置错误! 请检查系统配置:[" + e.getMessage() + "]");
        }
    }

    public void createBucket() {
        try {
            String bucketName = properties.getBucketName();
            if (client.doesBucketExistV2(bucketName)) {
                return;
            }
            CreateBucketRequest createBucketRequest = new CreateBucketRequest(bucketName);
            AccessPolicyType accessPolicy = getAccessPolicy();
            createBucketRequest.setCannedAcl(accessPolicy.getAcl());
            client.createBucket(createBucketRequest);
            client.setBucketPolicy(bucketName, getPolicy(bucketName, accessPolicy.getPolicyType()));
        } catch (Exception e) {
            throw new OssException("创建Bucket失败, 请核对配置信息:[" + e.getMessage() + "]");
        }
    }
}

文件上传的流程

前端组件 el-upload

<el-upload
  multiple
  :action="uploadImgUrl"
  list-type="picture-card"
  :on-success="handleUploadSuccess"
  :before-upload="handleBeforeUpload"
  :limit="limit"
  :on-error="handleUploadError"
  :on-exceed="handleExceed"
  ref="imageUpload"
  :before-remove="handleDelete"
  :show-file-list="true"
  :headers="headers"
  :file-list="fileList"
  :on-preview="handlePictureCardPreview"
  :class="{ hide: fileList.length >= limit }"
>
<script setup lang="ts">
// 上传的图片服务器地址
const uploadImgUrl = ref(baseUrl + "/resource/oss/upload");   

// 查询OSS对象存储列表
export function listOss(query: OssQuery): AxiosPromise<OssVO[]> {
  return request({
    url: '/resource/oss/list',
    method: 'get',
    params: query
  });
}

// 删除OSS对象存储
export function delOss(ossId: string | number | Array<string | number>) {
  return request({
    url: '/resource/oss/' + ossId,
    method: 'delete'
  });
}
</script>    

SysOssController.java

/**
 * 上传OSS对象存储
 *
 * @param file 文件
 */
@SaCheckPermission("system:oss:upload")
@Log(title = "OSS对象存储", businessType = BusinessType.INSERT)
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public R<SysOssUploadVo> upload(@RequestPart("file") MultipartFile file) {
    if (ObjectUtil.isNull(file)) {
        return R.fail("上传文件不能为空");
    }
    SysOssVo oss = ossService.upload(file);
    SysOssUploadVo uploadVo = new SysOssUploadVo();
    uploadVo.setUrl(oss.getUrl());
    uploadVo.setFileName(oss.getOriginalName());
    uploadVo.setOssId(oss.getOssId().toString());
    return R.ok(uploadVo);
}

SysOssServiceImpl.java

@Override
public SysOssVo upload(MultipartFile file) {
    String originalfileName = file.getOriginalFilename();
    String suffix = StringUtils.substring(originalfileName, originalfileName.lastIndexOf("."), originalfileName.length());
    OssClient storage = OssFactory.instance();
    UploadResult uploadResult;
    try {
        uploadResult = storage.uploadSuffix(file.getBytes(), suffix, file.getContentType());
    } catch (IOException e) {
        throw new ServiceException(e.getMessage());
    }
    // 保存文件信息
    return buildResultEntity(originalfileName, suffix, storage.getConfigKey(), uploadResult);
}

private SysOssVo matchingUrl(SysOssVo oss) {
    OssClient storage = OssFactory.instance(oss.getService());
    // 仅修改桶类型为 private 的URL,临时URL时长为120s
    if (AccessPolicyType.PRIVATE == storage.getAccessPolicy()) {
        oss.setUrl(storage.getPrivateUrl(oss.getFileName(), 120));
    }
    return oss;
}

如果 Minio 存储桶设置的是私有类型,这里会生成一个2分钟的临时访问路径,也就是说,这个路径可以在任何地方访问,但是两分钟后会自动失效。
后续重新访问存储桶文件时,会重新生成新的临时路径

OssClient.java

public UploadResult upload(InputStream inputStream, String path, String contentType) {
    if (!(inputStream instanceof ByteArrayInputStream)) {
        inputStream = new ByteArrayInputStream(IoUtil.readBytes(inputStream));
    }
    try {
        ObjectMetadata metadata = new ObjectMetadata();
        metadata.setContentType(contentType);
        metadata.setContentLength(inputStream.available());
        PutObjectRequest putObjectRequest = new PutObjectRequest(properties.getBucketName(), path, inputStream, metadata);
        // 设置上传对象的 Acl 为公共读
        putObjectRequest.setCannedAcl(getAccessPolicy().getAcl());
        client.putObject(putObjectRequest);
    } catch (Exception e) {
        throw new OssException("上传文件失败,请检查配置信息:[" + e.getMessage() + "]");
    }
    return UploadResult.builder().url(getUrl() + "/" + path).filename(path).build();
}

public String getPath(String prefix, String suffix) {
    // 生成uuid
    String uuid = IdUtil.fastSimpleUUID();
    // 文件路径
    String path = DateUtils.datePath() + "/" + uuid;
    if (StringUtils.isNotBlank(prefix)) {
        path = prefix + "/" + path;
    }
    return path + suffix;
}

getPath 方法重新生成上传的文件路径和文件名

S3 api 上传步骤

// 创建 ObjectMetadata 对象
ObjectMetadata metadata = new ObjectMetadata();
// 创建 PutObjectRequest 对象
PutObjectRequest putObjectRequest = new PutObjectRequest(properties.getBucketName(), path, inputStream, metadata);
// 上传
client.putObject(putObjectRequest);

文件删除

前端 el-upload 组件提供了删除文件功能

// 删除OSS对象存储
export function delOss(ossId: string | number | Array<string | number>) {
  return request({
    url: '/resource/oss/' + ossId,
    method: 'delete'
  });
}
/**
 * 删除OSS对象存储
 *
 * @param ossIds OSS对象ID串
 */
@SaCheckPermission("system:oss:remove")
@Log(title = "OSS对象存储", businessType = BusinessType.DELETE)
@DeleteMapping("/{ossIds}")
public R<Void> remove(@NotEmpty(message = "主键不能为空")
                      @PathVariable Long[] ossIds) {
    return toAjax(ossService.deleteWithValidByIds(List.of(ossIds), true));
}

@Override
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
    if (isValid) {
        // 做一些业务上的校验,判断是否需要校验
    }
    // 根据文件 ID 查询文件信息
    List<SysOss> list = baseMapper.selectBatchIds(ids);
    for (SysOss sysOss : list) {
        OssClient storage = OssFactory.instance(sysOss.getService());
        storage.delete(sysOss.getUrl());
    }
    return baseMapper.deleteBatchIds(ids) > 0;
}

OssClient.java

public void delete(String path) {
    path = path.replace(getUrl() + "/", "");
    try {
        client.deleteObject(properties.getBucketName(), path);
    } catch (Exception e) {
        throw new OssException("删除文件失败,请检查配置信息:[" + e.getMessage() + "]");
    }
}

文件下载

前端 oss 文件下载功能,这里使用的返回类型是 blob,表示一个不可变、原始数据的类文件对象

async oss(ossId: string | number) {
    const url = baseURL + '/resource/oss/download/' + ossId;
    downloadLoadingInstance = ElLoading.service({ text: '正在下载数据,请稍候', background: 'rgba(0, 0, 0, 0.7)' });
    try {
      const res = await axios({
        method: 'get',
        url: url,
        responseType: 'blob',
        headers: globalHeaders()
      });
      const isBlob = blobValidate(res.data);
      if (isBlob) {
        const blob = new Blob([res.data], { type: 'application/octet-stream' });
        FileSaver.saveAs(blob, decodeURIComponent(res.headers['download-filename'] as string));
      } else {
        this.printErrMsg(res.data);
      }
      downloadLoadingInstance.close();
    } catch (r) {
      console.error(r);
      ElMessage.error('下载文件出现错误,请联系管理员!');
      downloadLoadingInstance.close();
    }
  },
/**
 * 下载OSS对象
 *
 * @param ossId OSS对象ID
 */
@SaCheckPermission("system:oss:download")
@GetMapping("/download/{ossId}")
public void download(@PathVariable Long ossId, HttpServletResponse response) throws IOException {
    ossService.download(ossId, response);
}

SysOssServiceImpl.java

@Override
public void download(Long ossId, HttpServletResponse response) throws IOException {
    SysOssVo sysOss = SpringUtils.getAopProxy(this).getById(ossId);
    if (ObjectUtil.isNull(sysOss)) {
        throw new ServiceException("文件数据不存在!");
    }
    FileUtils.setAttachmentResponseHeader(response, sysOss.getOriginalName());
    response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE + "; charset=UTF-8");
    OssClient storage = OssFactory.instance(sysOss.getService());
    try(InputStream inputStream = storage.getObjectContent(sysOss.getUrl())) {
        int available = inputStream.available();
        IoUtil.copy(inputStream, response.getOutputStream(), available);
        response.setContentLength(available);
    } catch (Exception e) {
        throw new ServiceException(e.getMessage());
    }
}

@Cacheable(cacheNames = CacheNames.SYS_OSS, key = "#ossId")
@Override
public SysOssVo getById(Long ossId) {
    return baseMapper.selectVoById(ossId);
}

这个在调用 getById 方法时,并不是直接调用的,而是使用了 aop 切面,为什么呢?在这段代码中,通过 SpringUtils.getAopProxy(this) 获取代理对象进行方法调用的主要目的是确保方法调用时能够触发 AOP 的增强逻辑,这里就是确保 Cacheable 注解的功能能够生效

文件下载时使用的是 application/octet-stream 二进制流格式,前端接收二进制数据后,再转化为文件

版权声明:除特殊说明,博客文章均为Gavin原创,依据CC BY-SA 4.0许可证进行授权,转载请附上出处链接及本声明。
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇