简介

写文章功能部分还有一个重要的功能没有完成,那就是图片上传。

图片上传这里主要介绍两种实现方式:1、使用 MinIO 搭建图片服务,完成图片上传功能。2、使用七牛云对象存储

使用 MinIO

简介

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

说明:

  • /data 为minio的数据
  • /root/.minio 为minio的配置
  • MINIO_ACCESS_KEY 和 MINIO_SECRET_KEY 需要自行配置

3、在当前目录下执行docker-compose up 命令,即可运行

运行MinIO

4、在本地浏览器上访问服务器的8884端口,即可看到 minio 的可视化界面,输入 MINIO_ACCESS_KEY 和 MINIO_SECRET_KEY 就可以登录,我们可以上传图片进行测试

MinIO浏览器界面

集成 SpringBoot

1、导入依赖

<!-- minio -->
<dependency>
    <groupId>io.minio</groupId>
    <artifactId>minio</artifactId>
    <version>3.0.10</version>
</dependency>
<!-- guava -->
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>29.0-jre</version>
</dependency>

说明:

  • minio 需要依赖 guava ,所以还要导入 guava ,不然会报错

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 盘中已经有图片存在

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 测试

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. 客户端向向服务端发送请求,获取上传图片需要的token
  2. 客户端带上 token 上传图片

代码实现

1、导入依赖

<dependency>
    <groupId>com.qiniu</groupId>
    <artifactId>qiniu-java-sdk</artifactId>
    <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

说明:

  • url:上传之后访问图片的地址,就是我们前面在七牛云配置的域名
  • access-key 和 secret-key 可以到“个人中心” --> “密钥管理” 获取:https://portal.qiniu.com/user/key
  • server:客户端上传地址,不同地区的上传图片的地址不同,可以参考:https://developer.qiniu.com/kodo/manual/1671/region-endpoint ,笔者的是华南地区,所以就用的上面的地址

空间信息:区域

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 测试

Swagger测试

6、这个时候,复制 token 然后使用postman上传图片

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);
}

说明:

  • 七牛云给我们返回的 json 不是我们自定义的格式,所以不能走 axios 的拦截器,所以这里就直接导入 axios 发送请求。

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、这里上传一张“好人”的图片

编辑器中上传图片

说明:

  • 查看控制台可见上传图片总共发送了两次请求,一次获取从服务端 token,一次上传图片

4、发布文章之后,可在博客中看到图片依然存在,上传图片搞定

博客中查看图片

参考代码:https://gitee.com/qianyucc/QBlog2/tree/v-12.0

Logo

前往低代码交流专区

更多推荐