一、MinIO简介及Linux安装运行
1、简介
MinIO 是一个基于Apache License v2.0开源协议的对象存储服务。它兼容亚马逊S3云存储服务接口,非常适合于存储大容量非结构化的数据,例如图片、视频、日志文件、备份数据和容器/虚拟机镜像等,而一个对象文件可以是任意大小,从几kb到最大5T不等。
MinIO是一个非常轻量的服务,可以很简单的和其他应用的结合,类似 NodeJS, Redis 或者 MySQL。
这里仅描述API的简单应用。
2、在Linux安装运行
官方中文文档:https://docs.min.io/cn/minio-quickstart-guide.html
按照地址下载执行文件,放在指定目录下,
(1)、执行截图中的命令可快速把服务运行起来。
文章图片
(2)、上面(1)是快速启动,一旦关闭命令窗口服务就挂了。下面命令可实现关闭命令窗口仍可运行并且能够设置账号密码、生成日志。
cd /root
# 设置账号
export MINIO_ACCESS_KEY=XXXXXX# 设置密码
export MINIO_SECRET_KEY=XXXXXX# nohup启动服务 指定文件存放路径 /root/data 还有设置日志文件路径 /root/minio/log
nohup /root/minio server /root/data > /root/log/minio.log 2>&1 &
用http://IP:端口/minio/login(端口默认是9000,我这里是9001)运行之后输入账号密码登录。
文章图片
二、Spring Boot 配置MinIO
1、application.yml配置
# 图片服务器 minio配置
minio:
ip: xxxxxxx:9001
# minio登录账号密码
accessKey: xxxxxxx
secretKey: xxxxxxxx## 桶名(文件夹)命名规则要符合 亚马逊S3标准 详情可看http://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html
bucketName:
## 照片文件夹
facility: facility-photos
2、MinIO API依赖
io.minio
minio
5.0.2
3、操作API 类
更多Java Client API 请参考https://docs.min.io/cn/java-client-api-reference.html
package zondy.config;
import io.minio.MinioClient;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import zondy.entity.enums.ResultEnum;
import zondy.utils.R;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @ClassName Minio
* @Description Minio文件存储云服务相关工具类api文档:https://docs.min.io/cn/java-client-api-reference.html
* @Author sya
* @Date 2019/8/1 20:05
**/
@Component
@Slf4j
public class Minio {/**
* 服务器地址
*/
@Value("${minio.ip}")
private String ip;
/**
* 登录账号
*/
@Value("${minio.accessKey}")
private String accessKey;
/**
* 登录密码
*/
@Value("${minio.secretKey}")
private String secretKey;
/**
* 缩略图大小
*/
@Value("${minio.thumbor.width}")
private String thumborWidth;
/**
* Minio文件上传
* @param file 文件实体
* @param fileName 修饰过的文件名 非源文件名
* @param bucketName 所存文件夹(桶名)
* @return
*/
public R minioUpload(MultipartFile file, String fileName, String bucketName) {
try {
MinioClient minioClient = new MinioClient("http://" + ip, accessKey, secretKey);
boolean bucketExists = minioClient.bucketExists(bucketName);
if (bucketExists) {
log.info("仓库" + bucketName + "已经存在,可直接上传文件。");
} else {
minioClient.makeBucket(bucketName);
}
if (file.getSize() <= 20971520) {
// fileName为空,说明要使用源文件名上传
if (fileName == null) {
fileName = file.getOriginalFilename();
fileName = fileName.replaceAll(" ", "_");
}// minio仓库名
minioClient.putObject(bucketName, fileName, file.getInputStream(), file.getContentType());
log.info("成功上传文件 " + fileName + " 至 " + bucketName);
String fileUrl = bucketName + "/" + fileName;
Map map = new HashMap();
map.put("fileUrl", fileUrl);
map.put("bucketName", bucketName);
map.put("originFileName", fileName);
return R.ok(map);
} else {
throw new Exception("请上传小于20mb的文件");
}} catch (Exception e) {
e.printStackTrace();
if (e.getMessage().contains("ORA")) {
return R.error("上传失败:【查询参数错误】");
}
return R.error("上传失败:【" + e.getMessage() + "】");
}
}/**
* 判断文件是否存在
* @param fileName 文件名
* @param bucketName 桶名(文件夹)
* @return
*/
public boolean isFileExisted(String fileName, String bucketName) {
InputStream inputStream = null;
try {
MinioClient minioClient = new MinioClient("http://" + ip, accessKey, secretKey);
inputStream = minioClient.getObject(bucketName, fileName);
if (inputStream != null) {
return true;
}
} catch (Exception e) {
return false;
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return false;
}/**
* 删除文件
* @param bucketName 桶名(文件夹)
* @param fileName 文件名
* @return
*/
public boolean delete(String bucketName,String fileName) {
try {
MinioClient minioClient = new MinioClient("http://" + ip, accessKey, secretKey);
minioClient.removeObject(bucketName,fileName);
return true;
} catch (Exception e) {
log.error(e.getMessage());
return false;
}
}/**
* 下载文件
* @param objectName 文件名
* @param bucketName 桶名(文件夹)
* @param response
* @return
*/
public R downloadFile(String objectName,String bucketName, HttpServletResponse response) {
try {
MinioClient minioClient = new MinioClient("http://" + ip, accessKey, secretKey);
InputStream file = minioClient.getObject(bucketName,objectName);
String filename = new String(objectName.getBytes("ISO8859-1"), StandardCharsets.UTF_8);
response.setHeader("Content-Disposition", "attachment;
filename=" + filename);
ServletOutputStream servletOutputStream = response.getOutputStream();
int len;
byte[] buffer = new byte[1024];
while((len=file.read(buffer)) > 0){
servletOutputStream.write(buffer, 0, len);
}
servletOutputStream.flush();
file.close();
servletOutputStream.close();
return R.ok(objectName + "下载成功");
} catch (Exception e) {
e.printStackTrace();
if (e.getMessage().contains("ORA")) {
return R.error("下载失败:【查询参数错误】");
}
return R.error("下载失败:【" + e.getMessage() + "】");
}
}/**
* 获取文件流
* @param objectName 文件名
* @param bucketName 桶名(文件夹)
* @return
*/
public InputStream getFileInputStream(String objectName,String bucketName) {
try {
MinioClient minioClient = new MinioClient("http://" + ip, accessKey, secretKey);
return minioClient.getObject(bucketName,objectName);
} catch (Exception e) {
e.printStackTrace();
log.error(e.getMessage());
}
return null;
}}
4、公共接口调用
extractPathFromPattern 这个静态方法可以把指定url的后面“/”剩下的字符串全部截断当成参数,请求预览图片接口时,http://127.0.0.1:8083/sys/common/minio/view/facility-photos/1585303364202_1.jpg 后面的facility-photos/1585303364202_1.jpg会有参数的形式传过来。使用字符串分割得到桶名和文件名,最后获取文件流显示图片。
package zondy.common.system.controller;
import com.alibaba.fastjson.JSONObject;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import org.springframework.web.servlet.HandlerMapping;
import zondy.annotation.LoginUser;
import zondy.common.api.vo.Result;
import zondy.config.Minio;
import zondy.utils.R;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.Map;
@Slf4j
@RestController
@RequestMapping("/sys/common")
public class CommonController {
@Autowired
private Minio minio;
/**
* 桶名
*/
@Value("${minio.bucketName.facility}")
private String bucketName;
@PostMapping(value = "https://www.it610.com/facility/upload")
@ApiOperation(value = "https://www.it610.com/article/图片上传")
@ResponseBody
public Result facilityUpload(HttpServletRequest request, HttpServletResponse response) {
Result result = new Result<>();
try {
MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;
MultipartFile mf = multipartRequest.getFile("file");
// 获取上传文件对象
String orgName = "";
String fileName = "";
if (mf != null){
orgName = mf.getOriginalFilename();
// 获取文件名
fileName = System.currentTimeMillis()+"_"+ orgName.replaceAll(" ", "_");
}
// 步骤一、判断文件是否存在过 存在则不能上传(Minio服务器上传同样位置的同样文件名的文件时,新的文件会把旧的文件覆盖掉)
boolean exist = minio.isFileExisted(fileName, bucketName);
if (exist) {
result.error500("文件已存在");
log.error("文件 " + fileName + " 已经存在");
return result;
}
// 步骤二、上传文件
R r = minio.minioUpload(mf,fileName,bucketName);
if (r.get("data") != null) {
Map map = (Map) r.get("data");
// 步骤三、将上传的文件信息返回
JSONObject obj = new JSONObject();
obj.put("fileInfo", map);
result.setResult(obj);
result.setSuccess(true);
}
result.setSuccess(false);
} catch (Exception e) {
result.setSuccess(false);
result.setMessage(e.getMessage());
log.error(e.getMessage(), e);
}
return result;
} /**
* 预览图片
* 请求地址:http://localhost:8080/common/minio/view/{user/20190119/e1fe9925bc315c60addea1b98eb1cb1349547719_1547866868179.jpg}
*
* @param request
* @param response
*/
@GetMapping(value = "https://www.it610.com/article/minio/view/**")
public void minioView(HttpServletRequest request, HttpServletResponse response) {
// ISO-8859-1 ==> UTF-8 进行编码转换
String imgPath = extractPathFromPattern(request);
// 其余处理略
InputStream inputStream = null;
OutputStream outputStream = null;
try {
String bucketName = "";
String fileName = "";
response.setContentType("image/jpeg;
charset=utf-8");
if (StringUtils.isNotEmpty(imgPath)){
String[] split = imgPath.split("/");
bucketName = split[0];
fileName = split[1];
}
inputStream =minio.getFileInputStream(fileName,bucketName);
outputStream = response.getOutputStream();
byte[] buf = new byte[1024];
int len;
while ((len = inputStream.read(buf)) > 0) {
outputStream.write(buf, 0, len);
}
response.flushBuffer();
} catch (IOException e) {
log.error("预览图片失败" + e.getMessage());
// e.printStackTrace();
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
log.error(e.getMessage(), e);
}
}
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) {
log.error(e.getMessage(), e);
}
}
} } /**
*把指定URL后的字符串全部截断当成参数
*这么做是为了防止URL中包含中文或者特殊字符(/等)时,匹配不了的问题
* @param request
* @return
*/
private static String extractPathFromPattern(final HttpServletRequest request) {
String path = (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE);
String bestMatchPattern = (String) request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
return new AntPathMatcher().extractPathWithinPattern(bestMatchPattern, path);
}
}
三、Vue调用接口实现上传和预览图片功能
1、FileModal.vue ___template部分
(1)、的:action="uploadAction" 发起上传图片事件,调用上传图片接口;
(2)、的:src="https://www.it610.com/article/getAvatarView()" 是预览图片,调用预览图片接口;
{{ title }}
文章图片
上传取消提交
2、FileModal.vue ___script部分
(1)、url:fileUpload对应上传文件到MinIO的接口,minioView对应从MinIO获取文件流并返回图片的接口。
(2)、当调用上传文件后,将返回的文件信息(桶名、文件名、文件存放路径等)存到一个全局实体model中,预览图片调用 getAvatarView(),其中filePath这个参数包含桶名和文件名,在后端可根据“/”切割获取。处理提交那里的addFacilityFile(formData) 是将文件信息上传到数据库的,此处不描述。
3、前端效果
选择上传前:
文章图片
选择上传后
文章图片
列表展示
文章图片
学习一种新的技术,需要一边研究一边应用到项目中,学习与实践要统筹兼顾,不积跬步无以至千里。以上有不足之处,请各位多多指教!
【#|Spring Boot + 对象存储服务MinIO API + Vue 实现图片上传以及展示】
推荐阅读
- 数据结构和算法|LeetCode 的正确使用方式
- #|7.分布式事务管理
- #|算法设计与分析(Java实现)——贪心算法(集合覆盖案例)
- #|算法设计与分析(Java实现)—— 动态规划 (0-1 背包问题)
- #|阿尔法点亮LED灯(一)汇编语言
- #|Multimedia
- #|ARM裸机开发(汇编LED灯实验(I.MX6UL芯片))
- 基础课|使用深度优先搜索(DFS)、广度优先搜索(BFS)、A* 搜索算法求解 (n^2 -1) 数码难题,耗时与内存占用(时空复杂度)对比(附((n^2 - 1) 数码问题控
- #|学习笔记 | Ch05 Pandas数据清洗 —— 缺失值、重复值、异常值
- win10|搏一搏 单车变摩托,是时候捣鼓一下家中的小米电视机啦。