概述
MinIO 是一款高性能、分布式的對象存儲系統。它是一款軟體産品, 可以100%的運作在标準硬體。即X86等低成本機器也能夠很好的運作MinIO。MinIO與傳統的存儲和其他的對象存儲不同的是:它一開始就針對性能要求更高的私有雲标準進行軟體架構設計。因為MinIO一開始就隻為對象存儲而設計。是以他采用了更易用的方式進行設計,它能實作對象存儲所需要的全部功能,在性能上也更加強勁,它不會為了更多的業務功能而妥協,失去MinIO的易用性、高效性。這樣的結果所帶來的好處是:它能夠更簡單的實作局有彈性伸縮能力的原生對象存儲服務。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 讀寫權限
建立項目 操作 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>
- 編輯配置檔案
修改MinIO相關配置application.properties
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>
運作項目
- 上傳頁面,批量上傳圖檔
- 上傳效果
- 檢視 MinIO Browser
- 清單頁面,下載下傳,删除,預覽操作
- 預覽圖檔
- 删除圖檔
- 下載下傳圖檔