概述
MinIO 是一款高性能、分布式的对象存储系统。它是一款软件产品, 可以100%的运行在标准硬件。即X86等低成本机器也能够很好的运行MinIO。MinIO与传统的存储和其他的对象存储不同的是:它一开始就针对性能要求更高的私有云标准进行软件架构设计。因为MinIO一开始就只为对象存储而设计。所以他采用了更易用的方式进行设计,它能实现对象存储所需要的全部功能,在性能上也更加强劲,它不会为了更多的业务功能而妥协,失去MinIO的易用性、高效性。这样的结果所带来的好处是:它能够更简单的实现局有弹性伸缩能力的原生对象存储服务。MinIO在传统对象存储用例(例如辅助存储,灾难恢复和归档)方面表现出色。同时,它在机器学习、大数据、私有云、混合云等方面的存储技术上也独树一帜。当然,也不排除数据分析、高性能应用负载、原生云的支持。
今天我们使用JAVA来操作一下MinIO。
Docker 安装MinIO
- 创建目录和赋予权限
mkdir -p /app/cloud/minio/data mkdir -p /app/cloud/minio/config chmod -R 777 /app/cloud/minio/data chmod -R 777 /app/cloud/minio/config
- 拉取镜像
docker pull minio:minio
- 创建容器
docker run -d -p 9000:9000 --name minio \ -e "MINIO_ACCESS_KEY=minio" \ -e "MINIO_SECRET_KEY=Aa123456" \ -v /app/cloud/minio/data:/data \ -v /app/cloud/minio/config:/root/.minio \ minio/minio server /data
- 浏览器访问http://192.168.1.6:9000账号 : minio 密码:Aa123456 登录右下角加号创建mybucket桶
image.png
- 开放 mybucket 读写权限
image.png
创建项目 操作 MinIO
- pom.xml 相关依赖
<dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>LATEST</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>io.minio</groupId> <artifactId>minio</artifactId> <version>7.0.1</version> </dependency> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.6</version> </dependency>
- 编辑配置文件
application.properties
修改MinIO相关配置
server.port=80 spring.application.name=book-minio spring.thymeleaf.cache=false spring.servlet.multipart.max-file-size = 10MB spring.servlet.multipart.max-request-size=100MB minio.endpoint=http://192.168.1.6:9000 minio.accesskey=minio minio.secretKey=Aa123456
- 连接 MinIO 配置
import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @Data @ConfigurationProperties(prefix = "minio") @Component public class MinioProp { private String endpoint; private String accesskey; private String secretKey; }
- 创建
MinioClient
import io.minio.MinioClient; import io.minio.errors.InvalidEndpointException; import io.minio.errors.InvalidPortException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class MinioConfiguration { @Autowired private MinioProp minioProp; @Bean public MinioClient minioClient() throws InvalidPortException, InvalidEndpointException { MinioClient client = new MinioClient(minioProp.getEndpoint(), minioProp.getAccesskey(), minioProp.getSecretKey()); return client; } }
- MinIO 查看桶列表,存入,删除 操作
MinioController
import com.alibaba.fastjson.JSON; import com.lab.book.minio.common.Res; import io.minio.MinioClient; import io.minio.ObjectStat; import io.minio.PutObjectOptions; import io.minio.Result; import io.minio.messages.Item; import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.IOUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.ui.ModelMap; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.InputStream; import java.net.URLEncoder; import java.text.DecimalFormat; import java.util.*; @Slf4j @RestController public class MinioController { @Autowired private MinioClient minioClient; private static final String MINIO_BUCKET = "mybucket"; @GetMapping("/list") public List<Object> list(ModelMap map) throws Exception { Iterable<Result<Item>> myObjects = minioClient.listObjects(MINIO_BUCKET); Iterator<Result<Item>> iterator = myObjects.iterator(); List<Object> items = new ArrayList<>(); String format = "{'fileName':'%s','fileSize':'%s'}"; while (iterator.hasNext()) { Item item = iterator.next().get(); items.add(JSON.parse(String.format(format, item.objectName(), formatFileSize(item.size())))); } return items; } @PostMapping("/upload") public Res upload(@RequestParam(name = "file", required = false) MultipartFile[] file) { Res res = new Res(); res.setCode(500); if (file == null || file.length == 0) { res.setMessage("上传文件不能为空"); return res; } List<String> orgfileNameList = new ArrayList<>(file.length); for (MultipartFile multipartFile : file) { String orgfileName = multipartFile.getOriginalFilename(); orgfileNameList.add(orgfileName); try { InputStream in = multipartFile.getInputStream(); minioClient.putObject(MINIO_BUCKET, orgfileName, in, new PutObjectOptions(in.available(), -1)); in.close(); } catch (Exception e) { log.error(e.getMessage()); res.setMessage("上传失败"); return res; } } Map<String, Object> data = new HashMap<String, Object>(); data.put("bucketName", MINIO_BUCKET); data.put("fileName", orgfileNameList); res.setCode(200); res.setMessage("上传成功"); res.setData(data); return res; } @RequestMapping("/download/{fileName}") public void download(HttpServletResponse response, @PathVariable("fileName") String fileName) { InputStream in = null; try { ObjectStat stat = minioClient.statObject(MINIO_BUCKET, fileName); response.setContentType(stat.contentType()); response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8")); in = minioClient.getObject(MINIO_BUCKET, fileName); IOUtils.copy(in, response.getOutputStream()); } catch (Exception e) { log.error(e.getMessage()); } finally { if (in != null) { try { in.close(); } catch (IOException e) { log.error(e.getMessage()); } } } } @DeleteMapping("/delete/{fileName}") public Res delete(@PathVariable("fileName") String fileName) { Res res = new Res(); res.setCode(200); try { minioClient.removeObject(MINIO_BUCKET, fileName); } catch (Exception e) { res.setCode(500); log.error(e.getMessage()); } return res; } private static String formatFileSize(long fileS) { DecimalFormat df = new DecimalFormat("#.00"); String fileSizeString = ""; String wrongSize = "0B"; if (fileS == 0) { return wrongSize; } if (fileS < 1024) { fileSizeString = df.format((double) fileS) + " B"; } else if (fileS < 1048576) { fileSizeString = df.format((double) fileS / 1024) + " KB"; } else if (fileS < 1073741824) { fileSizeString = df.format((double) fileS / 1048576) + " MB"; } else { fileSizeString = df.format((double) fileS / 1073741824) + " GB"; } return fileSizeString; } }
- Res 文件
import lombok.AllArgsConstructor; import lombok.NoArgsConstructor; import java.io.Serializable; @lombok.Data @AllArgsConstructor @NoArgsConstructor public class Res implements Serializable { private static final long serialVersionUID = 1L; private Integer code; private Object data = ""; private String message = ""; }
- 路由文件
RouterController
import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; @Controller public class RouterController { @GetMapping({"/", "/index.html"}) public String index() { return "index"; } @GetMapping({"/upload.html"}) public String upload() { return "upload"; } }
- 前端 列表页面 src\main\resources\templates\index.html
<!DOCTYPE html> <html lang="zh-cn"> <head> <meta charset="utf-8"/> <title>图片列表</title> <link rel="stylesheet" href="http://cdn.staticfile.org/element-ui/2.13.1/theme-chalk/index.css"> </head> <body> <div id="app"> <el-link icon="el-icon-upload" href="/upload.html">上传图片</el-link> <br/> <el-table :data="results" stripe style="width: 60%" @row-click="preview"> <el-table-column type="index" width="50"></el-table-column> <el-table-column prop="fileName" label="文件名" width="180"></el-table-column> <el-table-column prop="fileSize" label="文件大小"></el-table-column> <el-table-column label="操作"> <template slot-scope="scope"> <a :href="'/download/' + scope.row.fileName + ''" class="el-icon-download">下载</a> <a :href="'/delete/' + scope.row.fileName + ''" @click.prevent="deleteFile($event,scope.$index,results)" class="el-icon-delete">删除</a> </template> </el-table-column> </el-table> <br/> <el-link icon="el-icon-picture">预览图片</el-link> <br/> <div class="demo-image__preview" v-if="previewImg"> <el-image style="width: 100px; height: 100px" :src="imgSrc" :preview-src-list="imgList"></el-image> </div> </div> <script src="http://cdn.staticfile.org/vue/2.6.11/vue.min.js"></script> <script src="http://cdn.staticfile.org/axios/0.19.2/axios.min.js"></script> <script src="http://cdn.staticfile.org/element-ui/2.13.1/index.js"></script> <script> new Vue({ el: '#app', data: { bucketURL: 'http://192.168.1.6:9000/mybucket/', previewImg: true, results: [], imgSrc: '', imgList: [] }, methods: { init() { axios.get('/list').then(response => { this.results = response.data; if (this.results.length == 0) { this.imgSrc = ''; this.previewImg = false; } else { for (var i = 0; i < this.results.length; i++) { this.imgList.push(this.bucketURL + this.results[i].fileName); if (i == 0) { this.imgSrc = this.bucketURL + this.results[0].fileName; } } } }); }, preview(row, event, column) { this.imgSrc = this.bucketURL + row.fileName; this.previewImg = true; }, deleteFile(e,index,list) { axios.delete(e.target.href, {}).then(res => { if (res.data.code == 200) { this.$message('删除成功!'); list.splice(index, 1); this.previewImg = false; } else { this.$message('删除失败!'); } }); } }, mounted() { this.init(); } }); </script> </body> </html>
- 前端上传页面 src\main\resources\templates\upload.html
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>图片上传</title> <link rel="stylesheet" type="text/css" href="http://cdn.staticfile.org/webuploader/0.1.5/webuploader.css"> <script type="text/javascript" src="https://cdn.staticfile.org/jquery/3.5.0/jquery.min.js"></script> <script type="text/javascript" src="http://cdn.staticfile.org/webuploader/0.1.5/webuploader.min.js"></script> </head> <body> <div id="uploader-demo"> <div id="fileList" class="uploader-list"></div> <div id="filePicker">选择图片</div> </div> <br/> <a href="/index.html">返回图片列表页面</a> <script type="text/javascript"> var uploader = WebUploader.create({ auto: true, swf: 'http://cdn.staticfile.org/webuploader/0.1.5/Uploader.swf', server: '/upload', pick: '#filePicker', accept: { title: 'Images', extensions: 'gif,jpg,jpeg,bmp,png', mimeTypes: 'image/*' } }); uploader.on('fileQueued', function (file) { var $li = $( '<div id="' + file.id + '" class="file-item thumbnail">' + '<img>' + '<div class="info">' + file.name + '</div>' + '</div>' ), $img = $li.find('img'); var $list = $("#fileList"); $list.append($li); uploader.makeThumb(file, function (error, src) { if (error) { $img.replaceWith('<span>不能预览</span>'); return; } $img.attr('src', src); }, 100, 100); }); uploader.on('uploadProgress', function (file, percentage) { var $li = $('#' + file.id), $percent = $li.find('.progress span'); if (!$percent.length) { $percent = $('<p class="progress"><span></span></p>') .appendTo($li) .find('span'); } $percent.css('width', percentage * 100 + '%'); }); uploader.on('uploadSuccess', function (file) { $('#' + file.id).addClass('upload-state-done'); }); uploader.on('uploadError', function (file) { var $li = $('#' + file.id), $error = $li.find('div.error'); if (!$error.length) { $error = $('<div class="error"></div>').appendTo($li); } $error.text('上传失败'); }); uploader.on('uploadComplete', function (file) { $('#' + file.id).find('.progress').remove(); }); </script> </body> </html>
运行项目
image.png
- 上传页面,批量上传图片
image.png
- 上传效果
image.png
- 查看 MinIO Browser
image.png
- 列表页面,下载,删除,预览操作
image.png
- 预览图片
image.png
- 删除图片
image.png
- 下载图片
image.png
Java 操作 MinIO 官方 demo
https://github.com/minio/minio-java/tree/master/examples
好了,各位朋友们,本期的内容到此就全部结束啦,能看到这里的同学都是优秀的同学,下一个升职加薪的就是你了!