写文章功能部分还有一个重要的功能没有完成,那就是图片上传。
图片上传这里主要介绍两种实现方式:1、使用 MinIO 搭建图片服务,完成图片上传功能。2、使用七牛云对象存储
MinIO 是一个基于Apache License v2.0开源协议的对象存储服务。它兼容亚马逊S3云存储服务接口,非常适合于存储大容量非结构化的数据,例如图片、视频、日志文件、备份数据和容器/虚拟机镜像等,而一个对象文件可以是任意大小,从几kb到最大5T不等。
MinIO 中文文档:https://docs.min.io/cn/
这里笔者使用一个安装了 docker 的 CentOS7 系统的阿里云服务器来搭建服务
1、安装 docker-compose
# 下载二进制包
sudo curl -L "https://github.com/docker/compose/releases/download/1.24.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
# 将可执行权限应用于二进制文件
sudo chmod +x /usr/local/bin/docker-compose
# 创建软链
sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose
# 测试是否安装成功
docker-compose --version
2、新建一个目录,我这里就叫 test,编写 docker-compose.yml
mkdir /usr/local/test
cd /usr/local/test
vim docker-compose.yml
version: "2"
services:
minio-server:
image: minio/minio
container_name: minio-server
restart: always
ports:
- "8884:9000"
volumes:
- /minio/data:/data
- /minio/config:/root/.minio
environment:
MINIO_ACCESS_KEY: "AKIAIOSFODNN7EXAMPLE"
MINIO_SECRET_KEY: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
command: server /data
说明:
3、在当前目录下执行docker-compose up
命令,即可运行
4、在本地浏览器上访问服务器的8884端口,即可看到 minio 的可视化界面,输入 MINIO_ACCESS_KEY 和 MINIO_SECRET_KEY 就可以登录,我们可以上传图片进行测试
1、导入依赖
<dependency>
<groupId>io.miniogroupId>
<artifactId>minioartifactId>
<version>3.0.10version>
dependency>
<dependency>
<groupId>com.google.guavagroupId>
<artifactId>guavaartifactId>
<version>29.0-jreversion>
dependency>
说明:
2、在 application-dev.yml中增加minio配置
minio:
server: http://117.50.23.198:8884/
access-key: AKIAIOSFODNN7EXAMPLE
secret-key: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
bucket: qblog
3、在 ErrorInfoEnum 中增加两个枚举,表示图片下载失败和上传失败
FILE_UPLOAD_ERROR(5001, "图片上传失败"),
FILE_DOWNLOAD_ERROR(5002, "图片下载失败")
4、创建包 provider 用于存放第三方请求,创建 MinioFileProvider 类,用户封装使用minio实现图片上传和下载
package pers.qianyucc.qblog.provider;
import io.minio.*;
import io.minio.errors.*;
import lombok.extern.slf4j.*;
import org.springframework.beans.factory.annotation.*;
import org.springframework.stereotype.*;
import org.xmlpull.v1.*;
import pers.qianyucc.qblog.exception.*;
import java.io.*;
import java.security.*;
import static pers.qianyucc.qblog.model.enums.ErrorInfoEnum.FILE_DOWNLOAD_ERROR;
import static pers.qianyucc.qblog.model.enums.ErrorInfoEnum.FILE_UPLOAD_ERROR;
@Slf4j
@Component
public class MinioFileProvider {
@Value("${minio.access-key}")
private String accessKey;
@Value("${minio.secret-key}")
private String secretKey;
@Value("${minio.server}")
private String endPoint;
@Value("${minio.bucket}")
private String bucketName;
public void uploadFile(InputStream in, String fileName, String contentType) {
try {
MinioClient minioClient = new MinioClient(endPoint, accessKey, secretKey);
boolean isExist = minioClient.bucketExists(bucketName);
if (!isExist) {
minioClient.makeBucket(bucketName);
}
minioClient.putObject(bucketName, fileName, in, contentType);
in.close();
log.info("{} upload success", fileName);
} catch (MinioException | IOException | InvalidKeyException | NoSuchAlgorithmException | XmlPullParserException e) {
log.error("{} upload error: {}", fileName, e.getMessage());
throw new BlogException(FILE_UPLOAD_ERROR);
}
}
public InputStream downloadFile(String fileName) {
try {
MinioClient minioClient = new MinioClient(endPoint, accessKey, secretKey);
InputStream in = minioClient.getObject(bucketName, fileName);
return in;
} catch (MinioException | IOException | InvalidKeyException | NoSuchAlgorithmException | XmlPullParserException e) {
log.error("{} download error: {}", fileName, e.getMessage());
throw new BlogException(FILE_DOWNLOAD_ERROR);
}
}
}
5、新建测试类,测试功能是否可以实现对应功能
package pers.qianyucc.qblog.provider;
import cn.hutool.core.io.*;
import cn.hutool.core.util.*;
import lombok.extern.slf4j.*;
import org.junit.*;
import org.junit.runner.*;
import org.springframework.beans.factory.annotation.*;
import org.springframework.boot.test.context.*;
import org.springframework.test.context.junit4.*;
import pers.qianyucc.qblog.*;
import java.io.*;
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest(classes = {
BlogApplication.class})
public class MinioFileProviderTest {
@Autowired
private MinioFileProvider minioFileProvider;
@Test
public void testUploadFile() {
String originalFilename = "E:\\User\\Pictures\\Screenshots\\屏幕截图(405).png";
BufferedInputStream in = FileUtil.getInputStream(originalFilename);
String fileName = IdUtil.fastUUID();
String[] arr = originalFilename.split("\\.");
if (ArrayUtil.isNotEmpty(arr)) {
fileName += "." + arr[arr.length - 1];
}
log.info("fileName : {}", fileName + ".png");
minioFileProvider.uploadFile(in, fileName, "application/octet-stream");
}
@Test
public void testDownloadFile() {
InputStream in = minioFileProvider.downloadFile("c824916f-d85e-4509-b138-f74addb4e77e.png");
FileUtil.writeFromStream(in, "f:/demo.png");
}
}
先执行 testUploadFile ,等到打印了 fileName 之后再执行 testDownloadFile ,可以看到 F 盘中已经有图片存在
在浏览器登录minio也可以看到对应的图片
6、新建 FileController ,编写与文件相关的接口
package pers.qianyucc.qblog.controller;
import cn.hutool.core.util.*;
import io.swagger.annotations.*;
import org.springframework.beans.factory.annotation.*;
import org.springframework.util.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.*;
import pers.qianyucc.qblog.model.comm.*;
import pers.qianyucc.qblog.provider.*;
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
@Api("与文件操作相关的api接口")
@RestController
public class FileController {
@Autowired
private MinioFileProvider minioFileProvider;
@ApiOperation("上传图片接口")
@PostMapping("/file/image")
public Results uploadImage(
@ApiParam("要上传的文件")
@RequestParam("image") MultipartFile multipartFile) {
String originalFilename = multipartFile.getOriginalFilename();
String[] arr = originalFilename.split("\\.");
String fileName = IdUtil.simpleUUID();
if (ArrayUtil.isNotEmpty(arr)) {
fileName += "." + arr[arr.length - 1];
}
try {
InputStream inputStream = multipartFile.getInputStream();
minioFileProvider.uploadFile(inputStream, fileName, multipartFile.getContentType());
} catch (IOException e) {
e.printStackTrace();
}
return Results.ok("图片上传成功", "/file/image/" + fileName);
}
@ApiOperation("获取图片")
@GetMapping("/file/image/{fileName}.{type}")
public Results getImage(@PathVariable String fileName, @PathVariable String type, HttpServletResponse res) {
String fullName = fileName + "." + type;
try (ServletOutputStream out = res.getOutputStream(); InputStream in = minioFileProvider.downloadFile(fullName)) {
StreamUtils.copy(in, out);
} catch (IOException e) {
e.printStackTrace();
}
return Results.ok("获取图片成功", null);
}
}
7、使用 Swagger 测试
随后,在浏览器打开链接,即可看到图片,接口的响应速度很大一部分取决于服务器的带宽,如果感觉太慢的话,可以选择提升服务器带宽
要使用七牛云,需要先注册七牛云账号,再进行实名认证。之后参考官方文档创建空间即可:https://developer.qiniu.com/kodo/manual/1233/console-quickstart
七牛云对象存储功能在注册之后会免费送 10 G 的空间,但是需要注意的是,上传完图片之后我们会得到一个图片地址,这个地址可以绑定我们自己的域名,也可以使用七牛云的随机域名,但是七牛云的随机域名只有一个月的有效期。要想长久使用,我们需要绑定自己的已经备案的域名。
绑定域名之后,要配置域名的CNAME才能生效,可参考七牛云文档:https://developer.qiniu.com/fusion/kb/1322/how-to-configure-cname-domain-name
七牛云提供两种上传方式:服务端上传和客户端上传。详情可见 Java SDK 文档:https://developer.qiniu.com/kodo/sdk/1239/java
上面 MinIO 的例子就是服务端上传,这里介绍一下客户端上传。事实上,大多数情况下我们应该使用客户端上传,因为这样不用占用服务器的带宽。
客户端上传步骤:
1、导入依赖
<dependency>
<groupId>com.qiniugroupId>
<artifactId>qiniu-java-sdkartifactId>
<version>[7.2.0, 7.2.99]version>
dependency>
2、在 application-dev.yml 中添加七牛云配置
qiniu:
server: http://upload-z2.qiniup.com
url: http://images.codingli.xyz/
access-key: knQrLKEGHOLDFBmDuXpSt_yeeF2WI_OykM6IITNN
secret-key: Dss4nnv73fvn24mcw8dfbsewJdeTffwdkau5Ubvn
bucket: qimages-api
说明:
3、编写 QiniuFileProvider 类,添加获取token的方法
package pers.qianyucc.qblog.provider;
import com.qiniu.util.*;
import lombok.extern.slf4j.*;
import org.springframework.beans.factory.annotation.*;
import org.springframework.stereotype.*;
@Slf4j
@Component
public class QiniuFileProvider {
@Value("${qiniu.access-key}")
private String accessKey;
@Value("${qiniu.secret-key}")
private String secretKey;
@Value("${qiniu.bucket}")
private String bucketName;
public String getUploadToken() {
Auth auth = Auth.create(accessKey, secretKey);
String upToken = auth.uploadToken(bucketName);
return upToken;
}
}
4、在 FileController 中添加获取token的接口
package pers.qianyucc.qblog.controller;
import cn.hutool.core.map.*;
import cn.hutool.core.util.*;
import io.swagger.annotations.*;
import org.springframework.beans.factory.annotation.*;
import org.springframework.util.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.*;
import pers.qianyucc.qblog.model.comm.*;
import pers.qianyucc.qblog.provider.*;
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import java.util.*;
@Api("与文件操作相关的api接口")
@RestController
public class FileController {
@Autowired
private MinioFileProvider minioFileProvider;
@Autowired
private QiniuFileProvider qiniuFileProvider;
@Value("${qiniu.server}")
private String uploadUrl;
@Value("${qiniu.url}")
private String imageUrl;
// ...
@ApiOperation("获取七牛云上传凭证")
@GetMapping("/file/qiniu/token")
public Results<QiniuUploadInfoVO> getToken() {
String token = qiniuFileProvider.getUploadToken();
QiniuUploadInfoVO info = QiniuUploadInfoVO.builder()
.token(token)
.uploadUrl(uploadUrl)
.imageUrl(imageUrl)
.build();
return Results.ok("token获取成功", info);
}
}
其中 QiniuUploadInfoVO 为客户端上传图片需要的信息
package pers.qianyucc.qblog.model.vo;
import lombok.*;
@Data
@Builder
public class QiniuUploadInfoVO {
private String token;
private String imageUrl;
private String uploadUrl;
}
5、使用 Swagger 测试
6、这个时候,复制 token 然后使用postman上传图片
7、把返回的 hash 拼接到 imageUrl 后面,就可以访问我们刚才上传的图片了
接下来启动 vue-admin-template , 这里只演示基于七牛云的前端直传
注意如果之前改过 vue.config.js 中的 publicPath 的话,要将其该回来 publicPath: '/'
,再执行 npm run dev
命令
1、在 @/api/
文件夹下新建 file.js 文件,用于封装所有和文件有关的请求,这里实现两个方法:获取上传图片 token 的方法、上传图片到七牛云的方法
import request from '@/utils/request'
import axios from 'axios';
export function getQiniuToken() {
return request({
url: '/file/qiniu/token',
method: 'get'
});
}
export function uploadToQiniuCloud(url, formData) {
return axios({
url: url,
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data' },
}).then(res => res.data);
}
说明:
2、我们切换到“写文章”界面,通过参考 mavonEditor官方文档描述的上传方式,我们为 mavonEditor 组件添加上传图片的方法
<mavon-editor ref="md" @imgAdd="$imgAdd" v-model="article.content" class="editor" />
import {
getQiniuToken, uploadToQiniuCloud } from "@/api/file";
$imgAdd(position, $file) {
getQiniuToken().then((res) => {
const uploadInfo = res.data;
let formData = new FormData();
formData.append("file", $file);
formData.append("token", uploadInfo.token);
uploadToQiniuCloud(uploadInfo.uploadUrl, formData)
.then((res) => {
const hash = res.hash;
if (hash) {
// 这里uploadInfo.imageUrl + hash 为在编辑器中显示的图片链接
this.$refs.md.$img2Url(position, uploadInfo.imageUrl + hash);
} else {
this.$message({
message: "网络忙,图片上传失败",
type: "error",
duration: 5 * 1000,
});
}
})
.catch((err) => {
console.log(err);
this.$message({
message: "网络忙,图片上传失败",
type: "error",
duration: 5 * 1000,
});
});
});
},
3、这里上传一张“好人”的图片
说明:
4、发布文章之后,可在博客中看到图片依然存在,上传图片搞定
参考代码:https://gitee.com/qianyucc/QBlog2/tree/v-12.0