implements Serializable {
7 |
8 | private static final long serialVersionUID = 1L;
9 |
10 | /**
11 | * 状态码
12 | *
13 | * 200000 成功
14 | * 400000 参数错误
15 | * 400100 用户名或者密码错误
16 | * 401000 token失效,请重新登录
17 | * 403000 未授权的访问
18 | * 404000 请求的网页/路径不存在
19 | * 500000 系统错误:请联系管理员
20 | */
21 | private Integer code;
22 |
23 | /**
24 | * 对应状态码的,返回提醒信息
25 | *
26 | * 200000 成功
27 | * 400000 参数错误
28 | * 400100 用户名或者密码错误
29 | * 401000 token失效,请重新登录
30 | * 403000 未授权的访问
31 | * 404000 请求的网页/路径不存在
32 | * 500000 系统错误:请联系管理员
33 | */
34 | private String msg;
35 |
36 | /**
37 | * 数据对象
38 | */
39 | private T data;
40 |
41 | public Result() {
42 | this.code = ResultEnum.SUCCESS.getCode();
43 | this.msg = ResultEnum.SUCCESS.getMsg();
44 | this.data = null;
45 | }
46 |
47 | public Result(T data) {
48 | this.code = ResultEnum.SUCCESS.getCode();
49 | this.msg = ResultEnum.SUCCESS.getMsg();
50 | this.data = data;
51 | }
52 |
53 | public Result(Integer code, String msg) {
54 | this.code = code;
55 | this.msg = msg;
56 | this.data = null;
57 | }
58 |
59 | public Result(Integer code, String msg, T data) {
60 | this.code = code;
61 | this.msg = msg;
62 | this.data = data;
63 | }
64 |
65 | public static Result ok() {
66 | return new Result();
67 | }
68 |
69 | public static Result ok(T data) {
70 | return new Result(data);
71 | }
72 |
73 | public static Result error() {
74 | return new Result(ResultEnum.ERROR_500.getCode(), ResultEnum.ERROR_500.getMsg());
75 | }
76 |
77 | public static Result error(String msg) {
78 | return new Result(ResultEnum.ERROR_500.getCode(), msg);
79 | }
80 |
81 | public static Result error(Integer code, String msg) {
82 | return new Result(code, msg);
83 | }
84 |
85 | public static Result error(Integer code, String msg, T data) {
86 | return new Result(code, msg, data);
87 | }
88 |
89 | public static long getSerialVersionUID() {
90 | return serialVersionUID;
91 | }
92 |
93 | public Integer getCode() {
94 | return code;
95 | }
96 |
97 | public void setCode(Integer code) {
98 | this.code = code;
99 | }
100 |
101 | public String getMsg() {
102 | return msg;
103 | }
104 |
105 | public void setMsg(String msg) {
106 | this.msg = msg;
107 | }
108 |
109 | public T getData() {
110 | return data;
111 | }
112 |
113 | public void setData(T data) {
114 | this.data = data;
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/MinIoDemo/MinIoDemo/src/main/java/com/iredbook/result/ResultEnum.java:
--------------------------------------------------------------------------------
1 | package com.iredbook.result;
2 |
3 |
4 | public enum ResultEnum {
5 | //未知错误
6 |
7 | //默认成功
8 | /**
9 | * SUCCESS 成功
10 | *
11 | * ERROR_301 TOKEN失效,需要重新登录
12 | * ERROR_400 请求无效
13 | * ERROR_401 未授权访问
14 | * ERROR_404 请求的网页/路径不存在
15 | * ERROR_500 未知错误
16 | * SUCCESS_201 操作异常,需要进行验证
17 | */
18 | SUCCESS(200000, "成功"),
19 | ERROR_400(400000, "参数错误"),
20 | ERROR_400100(400100, "用户名或者密码错误"),
21 | ERROR_401(401000, "token失效,请重新登录"),
22 | ERROR_403(403000, "未授权的访问"),
23 | ERROR_404(404000, "请求的网页/路径不存在"),
24 | ERROR_500(500000, "系统错误:请联系管理员"),
25 |
26 |
27 | ERROR_500100(500100, "获取用户保健卡号失败");
28 |
29 | private Integer code;
30 | private String msg;
31 |
32 | ResultEnum(Integer code, String msg) {
33 | this.code = code;
34 | this.msg = msg;
35 | }
36 |
37 | public Integer getCode() {
38 | return code;
39 | }
40 |
41 | public String getMsg() {
42 | return msg;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/MinIoDemo/MinIoDemo/src/main/java/com/iredbook/service/SysUploadTaskService.java:
--------------------------------------------------------------------------------
1 | package com.iredbook.service;
2 |
3 |
4 | import com.iredbook.dto.TaskInfoDTO;
5 | import com.iredbook.bo.InitTaskParam;
6 | import com.iredbook.pojo.SysUploadTask;
7 |
8 | import java.util.Map;
9 |
10 | /**
11 | * 分片上传-分片任务记录(SysUploadTask)表服务接口
12 | *
13 | * @since 2022-08-22 17:47:30
14 | */
15 | public interface SysUploadTaskService {
16 |
17 | /**
18 | * 查询是否上传过,若存在,返回TaskInfoDTO
19 | * @param identifier
20 | * @return
21 | */
22 | TaskInfoDTO getTaskInfo (String identifier);
23 |
24 | /**
25 | * 根据md5标识获取分片上传任务
26 | * @param identifier
27 | * @return
28 | */
29 | SysUploadTask getByIdentifier (String identifier);
30 |
31 | /**
32 | * 初始化一个任务
33 | */
34 | TaskInfoDTO initTask (InitTaskParam param);
35 |
36 | /**
37 | * 获取文件地址
38 | * @param bucket
39 | * @param objectKey
40 | * @return
41 | */
42 | String getPath (String bucket, String objectKey);
43 |
44 | /**
45 | * 生成预签名上传url
46 | * @param bucket 桶名
47 | * @param objectKey 对象的key
48 | * @param params 额外的参数
49 | * @return
50 | */
51 | String genPreSignUploadUrl (String bucket, String objectKey, Map params);
52 |
53 | /**
54 | * 合并分片
55 | * @param identifier
56 | */
57 | void merge (String identifier);
58 |
59 |
60 | }
61 |
--------------------------------------------------------------------------------
/MinIoDemo/MinIoDemo/src/main/java/com/iredbook/service/impl/SysUploadTaskServiceImpl.java:
--------------------------------------------------------------------------------
1 | package com.iredbook.service.impl;
2 |
3 | import cn.hutool.core.date.DateUtil;
4 | import cn.hutool.core.util.IdUtil;
5 | import cn.hutool.core.util.StrUtil;
6 | import com.amazonaws.HttpMethod;
7 | import com.amazonaws.services.s3.AmazonS3;
8 | import com.amazonaws.services.s3.model.*;
9 | import com.iredbook.dto.TaskInfoDTO;
10 | import com.iredbook.dto.TaskRecordDTO;
11 | import com.iredbook.config.MinIOConfig;
12 | import com.iredbook.bo.InitTaskParam;
13 | import com.iredbook.mapper.SysUploadTaskMapper;
14 | import com.iredbook.pojo.SysUploadTask;
15 | import com.iredbook.service.SysUploadTaskService;
16 | import org.springframework.beans.factory.annotation.Autowired;
17 | import org.springframework.http.MediaType;
18 | import org.springframework.http.MediaTypeFactory;
19 | import org.springframework.stereotype.Service;
20 | import tk.mybatis.mapper.entity.Example;
21 |
22 | import javax.annotation.Resource;
23 | import java.net.URL;
24 | import java.util.*;
25 | import java.util.stream.Collectors;
26 |
27 | /**
28 | * 分片上传-分片任务记录(SysUploadTask)表服务实现类
29 | *
30 | * @since 2022-08-22 17:47:31
31 | */
32 | @Service("sysUploadTaskService")
33 | public class SysUploadTaskServiceImpl implements SysUploadTaskService {
34 |
35 | // 预签名url过期时间(ms)
36 | private static final Long PRE_SIGN_URL_EXPIRE = 60 * 10 * 1000L;
37 |
38 | @Resource
39 | private AmazonS3 amazonS3;
40 |
41 | @Autowired
42 | private MinIOConfig minioProperties;
43 |
44 | @Autowired
45 | private SysUploadTaskMapper sysUploadTaskMapper;
46 |
47 | @Override
48 | public TaskInfoDTO getTaskInfo(String identifier) {
49 | SysUploadTask task = getByIdentifier(identifier);
50 | if (task == null) {
51 | return null;
52 | }
53 | TaskInfoDTO result = new TaskInfoDTO().setFinished(true).setTaskRecord(TaskRecordDTO.convertFromEntity(task)).setPath(getPath(task.getBucketName(), task.getObjectKey()));
54 |
55 | boolean doesObjectExist = amazonS3.doesObjectExist(task.getBucketName(), task.getObjectKey());
56 | if (!doesObjectExist) {
57 | // 未上传完,返回已上传的分片
58 | ListPartsRequest listPartsRequest = new ListPartsRequest(task.getBucketName(), task.getObjectKey(), task.getUploadId());
59 | PartListing partListing = amazonS3.listParts(listPartsRequest);
60 | result.setFinished(false).getTaskRecord().setExitPartList(partListing.getParts());
61 | }
62 | return result;
63 | }
64 |
65 | @Override
66 | public SysUploadTask getByIdentifier(String identifier) {
67 | Example taskExample = new Example(SysUploadTask.class);
68 | Example.Criteria criteria = taskExample.createCriteria();
69 | criteria.andEqualTo("fileIdentifier", identifier);
70 | SysUploadTask task = sysUploadTaskMapper.selectOneByExample(taskExample);
71 | return task;
72 | }
73 |
74 | @Override
75 | public TaskInfoDTO initTask(InitTaskParam param) {
76 |
77 | Date currentDate = new Date();
78 | String bucketName = minioProperties.getBucketName();
79 | String fileName = param.getFileName();
80 | String suffix = fileName.substring(fileName.lastIndexOf(".")+1, fileName.length());
81 | String key = StrUtil.format("{}/{}.{}", DateUtil.format(currentDate, "YYYY-MM-dd"), IdUtil.randomUUID(), suffix);
82 | String contentType = MediaTypeFactory.getMediaType(key).orElse(MediaType.APPLICATION_OCTET_STREAM).toString();
83 | ObjectMetadata objectMetadata = new ObjectMetadata();
84 | objectMetadata.setContentType(contentType);
85 | InitiateMultipartUploadResult initiateMultipartUploadResult = amazonS3
86 | .initiateMultipartUpload(new InitiateMultipartUploadRequest(bucketName, key).withObjectMetadata(objectMetadata));
87 | String uploadId = initiateMultipartUploadResult.getUploadId();
88 |
89 | SysUploadTask task = new SysUploadTask();
90 | int chunkNum = (int) Math.ceil(param.getTotalSize() * 1.0 / param.getChunkSize());
91 | // 获得全局唯一主键
92 | Long id = System.currentTimeMillis();
93 | task.setBucketName(minioProperties.getBucketName())
94 | .setChunkNum(chunkNum)
95 | .setChunkSize(param.getChunkSize())
96 | .setTotalSize(param.getTotalSize())
97 | .setFileIdentifier(param.getIdentifier())
98 | .setFileName(fileName)
99 | .setObjectKey(key)
100 | .setUploadId(uploadId)
101 | .setId(id);
102 | sysUploadTaskMapper.insert(task);
103 | return new TaskInfoDTO().setFinished(false).setTaskRecord(TaskRecordDTO.convertFromEntity(task)).setPath(getPath(bucketName, key));
104 | }
105 |
106 | @Override
107 | public String getPath(String bucket, String objectKey) {
108 | return StrUtil.format("{}/{}/{}", minioProperties.getEndpoint(), bucket, objectKey);
109 | }
110 |
111 | @Override
112 | public String genPreSignUploadUrl(String bucket, String objectKey, Map params) {
113 | Date currentDate = new Date();
114 | Date expireDate = DateUtil.offsetMillisecond(currentDate, PRE_SIGN_URL_EXPIRE.intValue());
115 | GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest(bucket, objectKey)
116 | .withExpiration(expireDate).withMethod(HttpMethod.PUT);
117 | if (params != null) {
118 | params.forEach((key, val) -> request.addRequestParameter(key, val));
119 | }
120 | URL preSignedUrl = amazonS3.generatePresignedUrl(request);
121 | return preSignedUrl.toString();
122 | }
123 |
124 | @Override
125 | public void merge(String identifier) {
126 | SysUploadTask task = getByIdentifier(identifier);
127 | if (task == null) {
128 | throw new RuntimeException("分片任务不存");
129 | }
130 |
131 | ListPartsRequest listPartsRequest = new ListPartsRequest(task.getBucketName(), task.getObjectKey(), task.getUploadId());
132 | PartListing partListing = amazonS3.listParts(listPartsRequest);
133 | List parts = partListing.getParts();
134 | if (!task.getChunkNum().equals(parts.size())) {
135 | // 已上传分块数量与记录中的数量不对应,不能合并分块
136 | throw new RuntimeException("分片缺失,请重新上传");
137 | }
138 | CompleteMultipartUploadRequest completeMultipartUploadRequest = new CompleteMultipartUploadRequest()
139 | .withUploadId(task.getUploadId())
140 | .withKey(task.getObjectKey())
141 | .withBucketName(task.getBucketName())
142 | .withPartETags(parts.stream().map(partSummary -> new PartETag(partSummary.getPartNumber(), partSummary.getETag())).collect(Collectors.toList()));
143 | CompleteMultipartUploadResult result = amazonS3.completeMultipartUpload(completeMultipartUploadRequest);
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/MinIoDemo/MinIoDemo/src/main/resources/application.yaml:
--------------------------------------------------------------------------------
1 | server:
2 | port: 3080
3 | spring:
4 | datasource:
5 | type: com.zaxxer.hikari.HikariDataSource # 数据源的类型,可以更改为其他的数据源配置,比如druid
6 | driver-class-name: com.mysql.cj.jdbc.Driver # mysql/MariaDB 的数据库驱动类名称
7 | url: jdbc:mysql://localhost:3306/my-red-book?characterEncoding=utf-8&serverTimezone=Hongkong
8 | username: root
9 | password: root
10 |
11 | # 整合mybatis
12 | mybatis:
13 | type-aliases-package: com.iredbook.pojo # 所有pojo类所在的包路径
14 | mapper-locations: classpath:mapper/*.xml # mapper映射文件
15 |
16 | # 通用mapper工具的配置
17 | mapper:
18 | mappers: com.iredbook.my.mapper.MyMapper # 配置MyMapper,包含了一些封装好的CRUD方法
19 | not-empty: false # 在进行数据库操作的时候,username != null 是否会追加 username != ''
20 | identity: MYSQL
21 |
22 | # MinIO 配置
23 | minio:
24 | endpoint: http://192.168.150.129:9000 # MinIO服务地址
25 | fileHost: http://192.168.150.129:9000 # 文件地址host
26 | bucketName: iredbook # 存储桶bucket名称
27 | accessKey: root # 用户名
28 | secretKey: root123456 # 密码
29 |
30 | custom:
31 | minio:
32 | bucket: iredbook
33 |
--------------------------------------------------------------------------------
/MinIoDemo/MinIoDemo/src/main/resources/mapper/SysUploadTaskMapper.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/MinIoDemo/MinIoDemo/src/test/java/com/example/miniodemo/MinIoDemoApplicationTests.java:
--------------------------------------------------------------------------------
1 | package com.example.miniodemo;
2 |
3 | import org.junit.jupiter.api.Test;
4 | import org.springframework.boot.test.context.SpringBootTest;
5 |
6 | @SpringBootTest
7 | class MinIoDemoApplicationTests {
8 |
9 | @Test
10 | void contextLoads() {
11 | }
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/MinIoDemo/README.md:
--------------------------------------------------------------------------------
1 | # 介绍
2 | **springboot整合MinIO实现视频的分片上传/断点续传**
3 |
4 | # 环境
5 | - **java 8**
6 |
7 | - **node 14**
8 | - **mysql 8**
9 |
10 |
11 | # 分片上传流程
12 | 
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/MinIoDemo/images/minio.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/robinsyn/MinIO_Demo/abe4621d05e02bdb84b5ee147ce764a58d0de996/MinIoDemo/images/minio.bmp
--------------------------------------------------------------------------------
/minio-upload-web/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | .DS_Store
12 | dist
13 | dist-ssr
14 | coverage
15 | *.local
16 |
17 | /cypress/videos/
18 | /cypress/screenshots/
19 |
20 | # Editor directories and files
21 | .vscode/*
22 | !.vscode/extensions.json
23 | .idea
24 | *.suo
25 | *.ntvs*
26 | *.njsproj
27 | *.sln
28 | *.sw?
29 |
--------------------------------------------------------------------------------
/minio-upload-web/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
3 | }
4 |
--------------------------------------------------------------------------------
/minio-upload-web/README.md:
--------------------------------------------------------------------------------
1 | # minio-upload-web
2 |
3 | This template should help get you started developing with Vue 3 in Vite.
4 |
5 | ## Recommended IDE Setup
6 |
7 | [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
8 |
9 | ## Customize configuration
10 |
11 | See [Vite Configuration Reference](https://vitejs.dev/config/).
12 |
13 | ## Project Setup
14 |
15 | ```sh
16 | npm install
17 | ```
18 |
19 | ### Compile and Hot-Reload for Development
20 |
21 | ```sh
22 | npm run dev
23 | ```
24 |
25 | ### Compile and Minify for Production
26 |
27 | ```sh
28 | npm run build
29 | ```
30 |
--------------------------------------------------------------------------------
/minio-upload-web/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | minio-upload
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/minio-upload-web/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "minio-upload-web",
3 | "version": "0.0.0",
4 | "scripts": {
5 | "dev": "vite",
6 | "build": "vite build",
7 | "preview": "vite preview --port 4173"
8 | },
9 | "dependencies": {
10 | "axios": "^0.27.2",
11 | "axios-extra": "^0.0.6",
12 | "element-plus": "^2.2.14",
13 | "promise-queue-plus": "^1.2.2",
14 | "spark-md5": "^3.0.2",
15 | "vue": "^3.2.37",
16 | "vue-router": "^4.1.3"
17 | },
18 | "devDependencies": {
19 | "@vitejs/plugin-vue": "^3.0.1",
20 | "@vitejs/plugin-vue-jsx": "^2.0.0",
21 | "vite": "^3.0.4"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/minio-upload-web/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/robinsyn/MinIO_Demo/abe4621d05e02bdb84b5ee147ce764a58d0de996/minio-upload-web/public/favicon.ico
--------------------------------------------------------------------------------
/minio-upload-web/src/App.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
9 |
12 |
--------------------------------------------------------------------------------
/minio-upload-web/src/assets/base.css:
--------------------------------------------------------------------------------
1 | /* color palette from */
2 | :root {
3 | --vt-c-white: #ffffff;
4 | --vt-c-white-soft: #f8f8f8;
5 | --vt-c-white-mute: #f2f2f2;
6 |
7 | --vt-c-black: #181818;
8 | --vt-c-black-soft: #222222;
9 | --vt-c-black-mute: #282828;
10 |
11 | --vt-c-indigo: #2c3e50;
12 |
13 | --vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
14 | --vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
15 | --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
16 | --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
17 |
18 | --vt-c-text-light-1: var(--vt-c-indigo);
19 | --vt-c-text-light-2: rgba(60, 60, 60, 0.66);
20 | --vt-c-text-dark-1: var(--vt-c-white);
21 | --vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
22 | }
23 |
24 | /* semantic color variables for this project */
25 | :root {
26 | --color-background: var(--vt-c-white);
27 | --color-background-soft: var(--vt-c-white-soft);
28 | --color-background-mute: var(--vt-c-white-mute);
29 |
30 | --color-border: var(--vt-c-divider-light-2);
31 | --color-border-hover: var(--vt-c-divider-light-1);
32 |
33 | --color-heading: var(--vt-c-text-light-1);
34 | --color-text: var(--vt-c-text-light-1);
35 |
36 | --section-gap: 160px;
37 | }
38 |
39 | @media (prefers-color-scheme: dark) {
40 | :root {
41 | --color-background: var(--vt-c-black);
42 | --color-background-soft: var(--vt-c-black-soft);
43 | --color-background-mute: var(--vt-c-black-mute);
44 |
45 | --color-border: var(--vt-c-divider-dark-2);
46 | --color-border-hover: var(--vt-c-divider-dark-1);
47 |
48 | --color-heading: var(--vt-c-text-dark-1);
49 | --color-text: var(--vt-c-text-dark-2);
50 | }
51 | }
52 |
53 | *,
54 | *::before,
55 | *::after {
56 | box-sizing: border-box;
57 | margin: 0;
58 | position: relative;
59 | font-weight: normal;
60 | }
61 |
62 | body {
63 | min-height: 100vh;
64 | color: var(--color-text);
65 | background: var(--color-background);
66 | transition: color 0.5s, background-color 0.5s;
67 | line-height: 1.6;
68 | font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
69 | Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
70 | font-size: 15px;
71 | text-rendering: optimizeLegibility;
72 | -webkit-font-smoothing: antialiased;
73 | -moz-osx-font-smoothing: grayscale;
74 | }
75 |
--------------------------------------------------------------------------------
/minio-upload-web/src/assets/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/minio-upload-web/src/assets/main.css:
--------------------------------------------------------------------------------
1 | @import "./base.css";
2 |
3 | #app {
4 | max-width: 1280px;
5 | margin: 0 auto;
6 | padding: 2rem;
7 |
8 | font-weight: normal;
9 | }
10 |
11 | a,
12 | .green {
13 | text-decoration: none;
14 | color: hsla(160, 100%, 37%, 1);
15 | transition: 0.4s;
16 | }
17 |
18 | @media (hover: hover) {
19 | a:hover {
20 | background-color: hsla(160, 100%, 37%, 0.2);
21 | }
22 | }
23 |
24 | @media (min-width: 1024px) {
25 | body {
26 | display: flex;
27 | place-items: center;
28 | }
29 |
30 | #app {
31 | display: grid;
32 | grid-template-columns: 1fr 1fr;
33 | padding: 0 2rem;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/minio-upload-web/src/lib/api.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import axiosExtra from 'axios-extra'
3 | const baseUrl = 'http://localhost:3080'
4 |
5 | const http = axios.create({
6 | baseURL: baseUrl
7 | })
8 |
9 | const httpExtra = axiosExtra.create({
10 | maxConcurrent: 5, //并发为1
11 | queueOptions: {
12 | retry: 3, //请求失败时,最多会重试3次
13 | retryIsJump: false //是否立即重试, 否则将在请求队列尾部插入重试请求
14 | }
15 | })
16 |
17 | http.interceptors.response.use(response => {
18 | return response.data
19 | })
20 |
21 | /**
22 | * 根据文件的md5获取未上传完的任务
23 | * @param identifier 文件md5
24 | * @returns {Promise>}
25 | */
26 | const taskInfo = (identifier) => {
27 | return http.get(`/v1/minio/tasks/${identifier}`)
28 | }
29 |
30 | /**
31 | * 初始化一个分片上传任务
32 | * @param identifier 文件md5
33 | * @param fileName 文件名称
34 | * @param totalSize 文件大小
35 | * @param chunkSize 分块大小
36 | * @returns {Promise>}
37 | */
38 | const initTask = ({ identifier, fileName, totalSize, chunkSize }) => {
39 | return http.post('/v1/minio/tasks', {identifier, fileName, totalSize, chunkSize})
40 | }
41 |
42 | /**
43 | * 获取预签名分片上传地址
44 | * @param identifier 文件md5
45 | * @param partNumber 分片编号
46 | * @returns {Promise>}
47 | */
48 | const preSignUrl = ({ identifier, partNumber }) => {
49 | return http.get(`/v1/minio/tasks/${identifier}/${partNumber}`)
50 | }
51 |
52 | /**
53 | * 合并分片
54 | * @param identifier
55 | * @returns {Promise>}
56 | */
57 | const merge = (identifier) => {
58 | return http.post(`/v1/minio/tasks/merge/${identifier}`)
59 | }
60 |
61 | export {
62 | taskInfo,
63 | initTask,
64 | preSignUrl,
65 | merge,
66 | httpExtra
67 | }
68 |
--------------------------------------------------------------------------------
/minio-upload-web/src/lib/md5.js:
--------------------------------------------------------------------------------
1 | import SparkMD5 from 'spark-md5'
2 | const DEFAULT_SIZE = 20 * 1024 * 1024
3 | const md5 = (file, chunkSize = DEFAULT_SIZE) => {
4 | return new Promise((resolve, reject) => {
5 | const startMs = new Date().getTime();
6 | let blobSlice =
7 | File.prototype.slice ||
8 | File.prototype.mozSlice ||
9 | File.prototype.webkitSlice;
10 | let chunks = Math.ceil(file.size / chunkSize);
11 | let currentChunk = 0;
12 | let spark = new SparkMD5.ArrayBuffer(); //追加数组缓冲区。
13 | let fileReader = new FileReader(); //读取文件
14 | fileReader.onload = function (e) {
15 | spark.append(e.target.result);
16 | currentChunk++;
17 | if (currentChunk < chunks) {
18 | loadNext();
19 | } else {
20 | const md5 = spark.end(); //完成md5的计算,返回十六进制结果。
21 | console.log('文件md5计算结束,总耗时:', (new Date().getTime() - startMs) / 1000, 's')
22 | resolve(md5);
23 | }
24 | };
25 | fileReader.onerror = function (e) {
26 | reject(e);
27 | };
28 |
29 | function loadNext() {
30 | console.log('当前part number:', currentChunk, '总块数:', chunks);
31 | let start = currentChunk * chunkSize;
32 | let end = start + chunkSize;
33 | (end > file.size) && (end = file.size);
34 | fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
35 | }
36 | loadNext();
37 | });
38 | }
39 |
40 | export default md5
41 |
--------------------------------------------------------------------------------
/minio-upload-web/src/main.js:
--------------------------------------------------------------------------------
1 | import { createApp } from 'vue'
2 | import App from './App.vue'
3 | import router from './router'
4 | import ElementPlus from 'element-plus'
5 | import 'element-plus/dist/index.css'
6 |
7 |
8 | const app = createApp(App)
9 |
10 | app.use(router).use(ElementPlus)
11 |
12 | app.mount('#app')
13 |
--------------------------------------------------------------------------------
/minio-upload-web/src/router/index.js:
--------------------------------------------------------------------------------
1 | import { createRouter, createWebHistory } from 'vue-router'
2 |
3 | const router = createRouter({
4 | history: createWebHistory(import.meta.env.BASE_URL),
5 | routes: [
6 | {
7 | path: '/',
8 | name: 'uploadPage',
9 | // route level code-splitting
10 | // this generates a separate chunk (About.[hash].js) for this route
11 | // which is lazy-loaded when the route is visited.
12 | component: () => import('../views/UploadPage.vue')
13 | }
14 | ]
15 | })
16 |
17 | export default router
18 |
--------------------------------------------------------------------------------
/minio-upload-web/src/views/UploadPage.vue:
--------------------------------------------------------------------------------
1 |
198 |
199 |
200 |
207 |
208 |
209 | 请拖拽文件到此处或 点击此处上传
210 |
211 |
212 |
213 |
214 |
215 |
--------------------------------------------------------------------------------
/minio-upload-web/vite.config.js:
--------------------------------------------------------------------------------
1 | import { fileURLToPath, URL } from 'node:url'
2 |
3 | import { defineConfig } from 'vite'
4 | import vue from '@vitejs/plugin-vue'
5 | import vueJsx from '@vitejs/plugin-vue-jsx'
6 |
7 | // https://vitejs.dev/config/
8 | export default defineConfig({
9 | plugins: [vue(), vueJsx()],
10 | resolve: {
11 | alias: {
12 | '@': fileURLToPath(new URL('./src', import.meta.url))
13 | }
14 | }
15 | })
16 |
--------------------------------------------------------------------------------