This commit is contained in:
LAN 2024-12-13 14:36:03 +08:00
commit fbfed73116
49 changed files with 4861 additions and 0 deletions

8
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

4
.idea/encodings.xml Normal file
View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" native2AsciiForPropertiesFiles="true" defaultCharsetForPropertiesFiles="UTF-8" />
</project>

68
.idea/misc.xml Normal file
View File

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectInspectionProfilesVisibleTreeState">
<entry key="Project Default">
<profile-state>
<expanded-state>
<State />
<State>
<id>JVM 语言</id>
</State>
<State>
<id>Java</id>
</State>
<State>
<id>Java 10Java 语言级别迁移帮助Java</id>
</State>
<State>
<id>Java 11Java 语言级别迁移帮助Java</id>
</State>
<State>
<id>Java 14Java 语言级别迁移帮助Java</id>
</State>
<State>
<id>Java 15Java 语言级别迁移帮助Java</id>
</State>
<State>
<id>Java 5Java 语言级别迁移帮助Java</id>
</State>
<State>
<id>Java 7Java 语言级别迁移帮助Java</id>
</State>
<State>
<id>Java 8Java 语言级别迁移帮助Java</id>
</State>
<State>
<id>Java 9Java 语言级别迁移帮助Java</id>
</State>
<State>
<id>Java 互操作问题Kotlin</id>
</State>
<State>
<id>Java 语言级别迁移帮助Java</id>
</State>
<State>
<id>JavaScript and TypeScript</id>
</State>
<State>
<id>Kotlin</id>
</State>
<State>
<id>有效性问题JavaScript and TypeScript</id>
</State>
<State>
<id>模块化问题Java</id>
</State>
<State>
<id>迁移Kotlin</id>
</State>
</expanded-state>
<selected-state>
<State>
<id>Android</id>
</State>
</selected-state>
</profile-state>
</entry>
</component>
</project>

1
README.md Normal file
View File

@ -0,0 +1 @@
# SpringBoot3+Minio+Vue3+ElementPlus实现断点续传/分片上传/文件秒传

33
minio-admin/.gitignore vendored Normal file
View File

@ -0,0 +1,33 @@
HELP.md
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/

View File

48
minio-admin/logs/info.log Normal file
View File

@ -0,0 +1,48 @@
11:56:44.842 [background-preinit] INFO o.h.v.i.util.Version - [<clinit>,21] - HV000001: Hibernate Validator 8.0.0.Final
11:56:44.859 [restartedMain] INFO c.m.MinioUploadFileApplication - [logStarting,51] - Starting MinioUploadFileApplication using Java 19.0.1 with PID 18717 (/Users/lan/minio-project-master/minio-admin/target/classes started by lan in /Users/lan/minio-project-master/minio-admin)
11:56:44.861 [restartedMain] INFO c.m.MinioUploadFileApplication - [logStartupProfileInfo,630] - No active profile set, falling back to 1 default profile: "default"
11:56:47.883 [restartedMain] INFO o.a.c.h.Http11NioProtocol - [log,173] - Initializing ProtocolHandler ["http-nio-9090"]
11:56:47.885 [restartedMain] INFO o.a.c.c.StandardService - [log,173] - Starting service [Tomcat]
11:56:47.885 [restartedMain] INFO o.a.c.c.StandardEngine - [log,173] - Starting Servlet engine: [Apache Tomcat/10.1.5]
11:56:47.954 [restartedMain] INFO o.a.c.c.C.[.[.[/] - [log,173] - Initializing Spring embedded WebApplicationContext
11:56:49.612 [restartedMain] INFO o.a.c.h.Http11NioProtocol - [log,173] - Starting ProtocolHandler ["http-nio-9090"]
11:56:49.642 [restartedMain] INFO c.m.MinioUploadFileApplication - [logStarted,57] - Started MinioUploadFileApplication in 5.269 seconds (process running for 7.524)
12:14:01.766 [http-nio-9090-exec-1] INFO o.a.c.c.C.[.[.[/] - [log,173] - Initializing Spring DispatcherServlet 'dispatcherServlet'
12:14:01.881 [http-nio-9090-exec-1] INFO c.m.c.FileMinioController - [checkFileUploadedByMd5,74] - REST: 通过查询 <036ee500fdb624314479780c6daf547d> 文件是否存在、是否进行断点续传
12:14:01.882 [http-nio-9090-exec-1] INFO c.m.s.i.UploadServiceImpl - [getByFileMD5,48] - tip message: 通过 <036ee500fdb624314479780c6daf547d> 查询redis是否存在
12:14:02.490 [http-nio-9090-exec-1] INFO c.m.s.i.UploadServiceImpl - [getByFileMD5,59] - tip message: 通过 <036ee500fdb624314479780c6daf547d> 查询mysql是否存在
12:14:02.681 [http-nio-9090-exec-1] INFO c.z.h.HikariDataSource - [getConnection,110] - HikariPool-1 - Starting...
12:14:02.962 [http-nio-9090-exec-1] INFO c.z.h.p.HikariPool - [checkFailFast,565] - HikariPool-1 - Added connection com.mysql.cj.jdbc.ConnectionImpl@1b0794cd
12:14:02.964 [http-nio-9090-exec-1] INFO c.z.h.HikariDataSource - [getConnection,123] - HikariPool-1 - Start completed.
12:14:03.297 [http-nio-9090-exec-2] INFO c.m.c.FileMinioController - [initMultiPartUpload,90] - REST: 通过 <FileUploadInfo(fileName=image.png, fileSize=1680162, contentType=application/octet-stream, chunkNum=1, uploadId=null, chunkSize=10485760, fileMd5=036ee500fdb624314479780c6daf547d, fileType=image, chunkUploadedList=[])> 初始化上传任务
12:14:03.301 [http-nio-9090-exec-2] INFO c.m.s.i.UploadServiceImpl - [initMultiPartUpload,85] - tip message: 通过 <FileUploadInfo(fileName=image.png, fileSize=1680162, contentType=application/octet-stream, chunkNum=1, uploadId=null, chunkSize=10485760, fileMd5=036ee500fdb624314479780c6daf547d, fileType=image, chunkUploadedList=[])> 开始初始化<分片上传>任务
12:14:03.834 [http-nio-9090-exec-2] INFO c.m.s.i.UploadServiceImpl - [initMultiPartUpload,91] - tip message: 当前分片数量 <1> 进行单文件上传
12:14:03.847 [http-nio-9090-exec-2] INFO c.m.c.MybatisPlusConfig - [insertFill,37] - start insert fill ....
12:14:03.941 [http-nio-9090-exec-2] INFO c.m.utils.MinioUtils - [getUploadObjectUrl,64] - tip message: 通过 <036ee500fdb624314479780c6daf547d.png-image> 开始单文件上传<minio>
12:14:03.947 [http-nio-9090-exec-2] INFO c.m.utils.MinioUtils - [getUploadObjectUrl,74] - tip message: 单个文件上传、成功
12:18:57.075 [SpringApplicationShutdownHook] INFO c.z.h.HikariDataSource - [close,350] - HikariPool-1 - Shutdown initiated...
12:18:57.082 [SpringApplicationShutdownHook] INFO c.z.h.HikariDataSource - [close,352] - HikariPool-1 - Shutdown completed.
12:19:02.637 [background-preinit] INFO o.h.v.i.util.Version - [<clinit>,21] - HV000001: Hibernate Validator 8.0.0.Final
12:19:02.653 [restartedMain] INFO c.m.MinioUploadFileApplication - [logStarting,51] - Starting MinioUploadFileApplication using Java 19.0.1 with PID 20456 (/Users/lan/minio-project-master/minio-admin/target/classes started by lan in /Users/lan/minio-project-master/minio-admin)
12:19:02.654 [restartedMain] INFO c.m.MinioUploadFileApplication - [logStartupProfileInfo,630] - No active profile set, falling back to 1 default profile: "default"
12:19:04.851 [restartedMain] INFO o.a.c.h.Http11NioProtocol - [log,173] - Initializing ProtocolHandler ["http-nio-9090"]
12:19:04.851 [restartedMain] INFO o.a.c.c.StandardService - [log,173] - Starting service [Tomcat]
12:19:04.852 [restartedMain] INFO o.a.c.c.StandardEngine - [log,173] - Starting Servlet engine: [Apache Tomcat/10.1.5]
12:19:04.915 [restartedMain] INFO o.a.c.c.C.[.[.[/] - [log,173] - Initializing Spring embedded WebApplicationContext
12:19:06.455 [restartedMain] INFO o.a.c.h.Http11NioProtocol - [log,173] - Starting ProtocolHandler ["http-nio-9090"]
12:19:06.558 [restartedMain] INFO c.m.MinioUploadFileApplication - [logStarted,57] - Started MinioUploadFileApplication in 4.334 seconds (process running for 5.695)
12:19:30.858 [http-nio-9090-exec-1] INFO o.a.c.c.C.[.[.[/] - [log,173] - Initializing Spring DispatcherServlet 'dispatcherServlet'
12:19:30.918 [http-nio-9090-exec-1] INFO c.m.c.FileMinioController - [checkFileUploadedByMd5,74] - REST: 通过查询 <dcf813524ee0f39b6d73aba673f2fe2d> 文件是否存在、是否进行断点续传
12:19:30.919 [http-nio-9090-exec-1] INFO c.m.s.i.UploadServiceImpl - [getByFileMD5,48] - tip message: 通过 <dcf813524ee0f39b6d73aba673f2fe2d> 查询redis是否存在
12:19:31.237 [http-nio-9090-exec-1] INFO c.m.s.i.UploadServiceImpl - [getByFileMD5,59] - tip message: 通过 <dcf813524ee0f39b6d73aba673f2fe2d> 查询mysql是否存在
12:19:31.343 [http-nio-9090-exec-1] INFO c.z.h.HikariDataSource - [getConnection,110] - HikariPool-1 - Starting...
12:19:31.551 [http-nio-9090-exec-1] INFO c.z.h.p.HikariPool - [checkFailFast,565] - HikariPool-1 - Added connection com.mysql.cj.jdbc.ConnectionImpl@42d42417
12:19:31.552 [http-nio-9090-exec-1] INFO c.z.h.HikariDataSource - [getConnection,123] - HikariPool-1 - Start completed.
12:19:31.708 [http-nio-9090-exec-2] INFO c.m.c.FileMinioController - [initMultiPartUpload,90] - REST: 通过 <FileUploadInfo(fileName=院徽.png, fileSize=26909, contentType=application/octet-stream, chunkNum=1, uploadId=null, chunkSize=10485760, fileMd5=dcf813524ee0f39b6d73aba673f2fe2d, fileType=image, chunkUploadedList=[])> 初始化上传任务
12:19:31.712 [http-nio-9090-exec-2] INFO c.m.s.i.UploadServiceImpl - [initMultiPartUpload,85] - tip message: 通过 <FileUploadInfo(fileName=院徽.png, fileSize=26909, contentType=application/octet-stream, chunkNum=1, uploadId=null, chunkSize=10485760, fileMd5=dcf813524ee0f39b6d73aba673f2fe2d, fileType=image, chunkUploadedList=[])> 开始初始化<分片上传>任务
12:19:32.184 [http-nio-9090-exec-2] INFO c.m.s.i.UploadServiceImpl - [initMultiPartUpload,91] - tip message: 当前分片数量 <1> 进行单文件上传
12:19:32.202 [http-nio-9090-exec-2] INFO c.m.c.MybatisPlusConfig - [insertFill,37] - start insert fill ....
12:19:32.279 [http-nio-9090-exec-2] INFO c.m.utils.MinioUtils - [getUploadObjectUrl,64] - tip message: 通过 <dcf813524ee0f39b6d73aba673f2fe2d.png-image> 开始单文件上传<minio>
12:19:32.287 [http-nio-9090-exec-2] INFO c.m.utils.MinioUtils - [getUploadObjectUrl,74] - tip message: 单个文件上传、成功
12:35:11.549 [SpringApplicationShutdownHook] INFO c.z.h.HikariDataSource - [close,350] - HikariPool-1 - Shutdown initiated...
12:35:11.575 [SpringApplicationShutdownHook] INFO c.z.h.HikariDataSource - [close,352] - HikariPool-1 - Shutdown completed.

104
minio-admin/pom.xml Normal file
View File

@ -0,0 +1,104 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.mmg</groupId>
<artifactId>minio-admin</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>minio-admin</name>
<description>minio-admin</description>
<properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>3.0.2</spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.50</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.5</version>
</dependency>
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.3.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.20</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>17</source>
<target>17</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,11 @@
package com.mmg;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MinioUploadFileApplication {
public static void main(String[] args) {
SpringApplication.run(MinioUploadFileApplication.class, args);
}
}

View File

@ -0,0 +1,72 @@
package com.mmg.config;
import com.google.common.collect.Multimap;
import io.minio.CreateMultipartUploadResponse;
import io.minio.ListPartsResponse;
import io.minio.MinioClient;
import io.minio.ObjectWriteResponse;
import io.minio.messages.Part;
public class CustomMinioClient extends MinioClient {
/**
* 继承父类
* @param client
*/
public CustomMinioClient(MinioClient client) {
super(client);
}
/**
* 初始化分片上传获取 uploadId
*
* @param bucket String 存储桶名称
* @param region String
* @param object String 文件名称
* @param headers Multimap<String, String> 请求头
* @param extraQueryParams Multimap<String, String>
* @return String
*/
public String initMultiPartUpload(String bucket, String region, String object, Multimap<String, String> headers, Multimap<String, String> extraQueryParams) throws Exception {
CreateMultipartUploadResponse response = this.createMultipartUpload(bucket, region, object, headers, extraQueryParams);
return response.result().uploadId();
}
/**
* 合并分片
*
* @param bucketName String 桶名称
* @param region String
* @param objectName String 文件名称
* @param uploadId String 上传的 uploadId
* @param parts Part[] 分片集合
* @param extraHeaders Multimap<String, String>
* @param extraQueryParams Multimap<String, String>
* @return ObjectWriteResponse
*/
public ObjectWriteResponse mergeMultipartUpload(String bucketName, String region, String objectName, String uploadId, Part[] parts, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) throws Exception {
return this.completeMultipartUpload(bucketName, region, objectName, uploadId, parts, extraHeaders, extraQueryParams);
}
/**
* 查询当前上传后的分片信息
*
* @param bucketName String 桶名称
* @param region String
* @param objectName String 文件名称
* @param maxParts Integer 分片数量
* @param partNumberMarker Integer 分片起始值
* @param uploadId String 上传的 uploadId
* @param extraHeaders Multimap<String, String>
* @param extraQueryParams Multimap<String, String>
* @return ListPartsResponse
*/
public ListPartsResponse listMultipart(String bucketName, String region, String objectName, Integer maxParts, Integer partNumberMarker, String uploadId, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) throws Exception {
return this.listParts(bucketName, region, objectName, maxParts, partNumberMarker, uploadId, extraHeaders, extraQueryParams);
}
}

View File

@ -0,0 +1,52 @@
package com.mmg.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.LocalDateTime;
/**
* MybatisPlus配置类
*
*/
@Slf4j
@Configuration
@MapperScan("com.mmg.mapper")
public class MybatisPlusConfig implements MetaObjectHandler {
/**
* 新的分页插件
* 需要设置 MybatisConfiguration#useDeprecatedExecutor = false
* 避免缓存出现问题(该属性会在旧插件移除后一同移除)
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
@Override
public void insertFill(MetaObject metaObject) {
log.info("start insert fill ....");
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); // 起始版本 3.3.0(推荐使用)
this.fillStrategy(metaObject, "createTime", LocalDateTime.now()); // 也可以使用(3.3.0 该方法有bug请升级到之后的版本如`3.3.1.8-SNAPSHOT`)
/* 上面选其一使用,下面的已过时(注意 strictInsertFill 有多个方法,详细查看源码) */
}
@Override
public void updateFill(MetaObject metaObject) {
log.info("start update fill ....");
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); // 起始版本 3.3.0(推荐使用)
this.fillStrategy(metaObject, "updateTime", LocalDateTime.now()); // 也可以使用(3.3.0 该方法有bug请升级到之后的版本如`3.3.1.8-SNAPSHOT`)
/* 上面选其一使用,下面的已过时(注意 strictUpdateFill 有多个方法,详细查看源码) */
}
}

View File

@ -0,0 +1,48 @@
package com.mmg.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
//编写我们自己的RedisTemplate
@Configuration
public class RedisConfig {
//固定redis模板在工作中拿去就可以用
@Bean
@SuppressWarnings("all")
//改成StringObject类型
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
//我们为了自己开发方便一般直接使用<String, Object>
RedisTemplate<String, Object> template = new RedisTemplate();
template.setConnectionFactory(factory);
//Json序列化配置
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
//String 的序列化
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
//key采用string的序列化方式
template.setKeySerializer(stringRedisSerializer);
//value的序列化方式采用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
//hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
//hash的value序列化方式采用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}

View File

@ -0,0 +1,44 @@
package com.mmg.config;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.alibaba.fastjson.support.config.FastJsonConfig;
import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @description Web应用程序配置
* @author LGY
* @date 2023/03/14 20:09
* @version 1.0.0
*/
@Configuration
public class WebAppConfigurer implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowCredentials(true)
.allowedOriginPatterns("*")
.allowedMethods("POST", "GET", "PUT", "OPTIONS", "DELETE")
.allowedHeaders("*");
}
//实体类属性为空时不进行序列化返回给前端
@Bean
public HttpMessageConverters fastJsonHttpMessageConverters() {
FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
FastJsonConfig fastJsonConfig = new FastJsonConfig();
fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat);
fastConverter.setFastJsonConfig(fastJsonConfig);
HttpMessageConverter<?> converter = fastConverter;
return new HttpMessageConverters(converter);
}
}

View File

@ -0,0 +1,131 @@
package com.mmg.controller;
import com.mmg.model.vo.FileUploadInfo;
import com.mmg.service.UploadService;
import com.mmg.utils.MinioUtils;
import com.mmg.utils.R;
import com.mmg.utils.RedisUtil;
import com.mmg.utils.RespEnum;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
/**
* minio上传流程
* <p>
* 1.检查数据库中是否存在上传文件
* <p>
* 2.根据文件信息初始化获取分片预签名url地址前端根据url地址上传文件
* <p>
* 3.上传完成后将分片上传的文件进行合并
* <p>
* 4.保存文件信息到数据库
*/
@RestController
@Slf4j
@RequestMapping("upload")
public class FileMinioController {
@Resource
private UploadService uploadService;
@Resource
private RedisUtil redisUtil;
@Resource
private MinioUtils minioUtils;
/**
* @param fileMD5 文件md5
* @return {@link R }
* @description 获取上传文件
* @author LGY
* @date 2023/04/26 16:00
*/
@GetMapping("/getUploadingFile/{fileMD5}")
public R getUploadingFile(@PathVariable String fileMD5) {
try {
if (StringUtils.isEmpty(fileMD5)) {
return R.error();
}
FileUploadInfo fileUploadInfo = (FileUploadInfo) redisUtil.get(fileMD5);
if (fileUploadInfo != null) {
// 查询上传后的分片数据
fileUploadInfo.setChunkUploadedList(minioUtils.getChunkByFileMD5(fileUploadInfo.getFileName(), fileUploadInfo.getUploadId(), fileUploadInfo.getFileType()));
return R.ok().setData(fileUploadInfo);
}
return R.error();
} catch (Exception e) {
return R.error(e.getMessage());
}
}
/**
* 校验文件是否存在
*
* @param md5 String
* @return ResponseResult<Object>
*/
@GetMapping("/multipart/check")
public R checkFileUploadedByMd5(@RequestParam("md5") String md5) {
log.info("REST: 通过查询 <{}> 文件是否存在、是否进行断点续传", md5);
if (StringUtils.isEmpty(md5)) {
log.error("查询文件是否存在、入参无效");
return R.error(RespEnum.ACCESS_PARAMETER_INVALID);
}
return uploadService.getByFileMD5(md5);
}
/**
* 分片初始化
*
* @param fileUploadInfo 文件信息
* @return ResponseResult<Object>
*/
@PostMapping("/multipart/init")
public R initMultiPartUpload(@RequestBody FileUploadInfo fileUploadInfo) {
log.info("REST: 通过 <{}> 初始化上传任务", fileUploadInfo);
return R.ok().setData(uploadService.initMultiPartUpload(fileUploadInfo));
}
/**
* 完成上传
*
* @param fileUploadInfo 文件信息
* @return ResponseResult<Object>
*/
@PostMapping("/multipart/merge")
public R completeMultiPartUpload(@RequestBody FileUploadInfo fileUploadInfo) {
log.info("REST: 通过 {} 合并上传任务", fileUploadInfo);
//合并文件
String url = uploadService.mergeMultipartUpload(fileUploadInfo);
//获取上传文件地址
if (!StringUtils.isEmpty(url)) {
return R.ok().setData(url);
}
return R.error();
}
@PostMapping("/multipart/uploadScreenshot")
public R uploaduploadScreenshot(@RequestPart("photos") MultipartFile[] photos,
@RequestParam("buckName") String buckName) {
log.info("REST: 上传文件信息 <{}> ", photos);
for (MultipartFile photo : photos) {
if (!photo.isEmpty()) {
uploadService.upload(photo, buckName);
}
}
return R.ok();
}
@RequestMapping("/createBucket")
public void createBucket(@RequestParam("bucketName") String bucketName) {
String bucket = minioUtils.createBucket(bucketName);
}
}

View File

@ -0,0 +1,15 @@
package com.mmg.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.mmg.model.po.Files;
/**
* <p>
* Mapper 接口
* </p>
*
* @author LGY
*/
public interface FilesMapper extends BaseMapper<Files> {
}

View File

@ -0,0 +1,65 @@
package com.mmg.model.po;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* <p>
*
* </p>
*
* @author LGY
*/
@Data
@TableName("files")
public class Files implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
@TableField("upload_id")
private String uploadId;
@TableField("file_md5")
private String fileMd5;
@TableField("url")
private String url;
@TableField("file_name")
private String fileName;
@TableField("bucket_name")
private String bucketName;
@TableField("file_type")
private String fileType;
@TableField("file_size")
private Long fileSize;
@TableField("chunk_size")
private Long chunkSize;
@TableField("chunk_num")
private Integer chunkNum;
@TableField("is_delete")
@TableLogic(value = "0",delval = "1")
private Boolean isDelete;
@TableField("enable")
private Boolean enable;
@TableField(value = "create_time", fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
}

View File

@ -0,0 +1,45 @@
package com.mmg.model.vo;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.experimental.Accessors;
import java.util.List;
@Data
@Accessors(chain = true)
public class FileUploadInfo {
@NotBlank(message = "文件名不能为空")
private String fileName;
@NotNull(message = "文件大小不能为空")
private Long fileSize;
@NotBlank(message = "Content-Type不能为空")
private String contentType;
@NotNull(message = "分片数量不能为空")
private Integer chunkNum;
@NotBlank(message = "uploadId 不能为空")
private String uploadId;
private Long chunkSize;
// 桶名称
//private String bucketName;
//md5
private String fileMd5;
//文件类型
private String fileType;
//已上传的分片索引+1
private List<Integer> chunkUploadedList;
}

View File

@ -0,0 +1,50 @@
package com.mmg.service;
import com.mmg.model.vo.FileUploadInfo;
import com.mmg.utils.R;
import org.springframework.web.multipart.MultipartFile;
import java.util.Map;
public interface UploadService {
/**
* 分片上传初始化
*
* @param fileUploadInfo
* @return Map<String, Object>
*/
Map<String, Object> initMultiPartUpload(FileUploadInfo fileUploadInfo);
/**
* 完成分片上传
*
* @param fileUploadInfo
* @return String
*/
String mergeMultipartUpload(FileUploadInfo fileUploadInfo);
/**
* 通过 md5 获取已上传的数据
* @param md5 String
* @return Mono<Map<String, Object>>
*/
R getByFileMD5(String md5);
/**
* 获取文件地址
* @param bucketName
* @param fileName
*
*/
String getFliePath(String bucketName, String fileName);
/**
* 单文件上传
* @param file
* @param bucketName
* @return
*/
String upload(MultipartFile file, String bucketName);
}

View File

@ -0,0 +1,155 @@
package com.mmg.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.mmg.mapper.FilesMapper;
import com.mmg.model.po.Files;
import com.mmg.model.vo.FileUploadInfo;
import com.mmg.service.UploadService;
import com.mmg.utils.MinioUtils;
import com.mmg.utils.R;
import com.mmg.utils.RedisUtil;
import com.mmg.utils.RespEnum;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
import java.util.Map;
@Slf4j
@Service
public class UploadServiceImpl implements UploadService {
@Resource
private FilesMapper filesMapper;
@Resource
private MinioUtils minioUtils;
@Resource
private RedisUtil redisUtil;
@Value("${minio.breakpoint-time}")
private Integer breakpointTime;
/**
* 通过 md5 获取已上传的数据断点续传
*
* @param md5 String
* @return Mono<Map < String, Object>>
*/
@Override
public R getByFileMD5(String md5) {
log.info("tip message: 通过 <{}> 查询redis是否存在", md5);
// 从redis获取文件名称和id
FileUploadInfo fileUploadInfo = (FileUploadInfo) redisUtil.get(md5);
if (fileUploadInfo != null) {
// 正在上传查询上传后的分片数据
List<Integer> chunkList = minioUtils.getChunkByFileMD5(fileUploadInfo.getFileName(), fileUploadInfo.getUploadId(), fileUploadInfo.getFileType());
fileUploadInfo.setChunkUploadedList(chunkList);
return R.ok(RespEnum.UPLOADING).setData(fileUploadInfo);
}
log.info("tip message: 通过 <{}> 查询mysql是否存在", md5);
// 查询数据库是否上传成功
Files one = filesMapper.selectOne(new LambdaQueryWrapper<Files>().eq(Files::getFileMd5, md5));
if (one != null) {
FileUploadInfo mysqlsFileUploadInfo = new FileUploadInfo();
BeanUtils.copyProperties(one, mysqlsFileUploadInfo);
return R.ok(RespEnum.UPLOADSUCCESSFUL).setData(mysqlsFileUploadInfo);
}
return R.ok(RespEnum.NOT_UPLOADED);
}
/**
* 文件分片上传
*
* @param fileUploadInfo
* @return Mono<Map < String, Object>>
*/
@Override
public Map<String, Object> initMultiPartUpload(FileUploadInfo fileUploadInfo) {
FileUploadInfo redisFileUploadInfo = (FileUploadInfo) redisUtil.get(fileUploadInfo.getFileMd5());
if (redisFileUploadInfo != null) {
fileUploadInfo = redisFileUploadInfo;
}
log.info("tip message: 通过 <{}> 开始初始化<分片上传>任务", fileUploadInfo);
// 获取桶
String bucketName = minioUtils.getBucketName(fileUploadInfo.getFileType());
// 单文件上传
if (fileUploadInfo.getChunkNum() == 1) {
log.info("tip message: 当前分片数量 <{}> 进行单文件上传", fileUploadInfo.getChunkNum());
Files files = saveFileToDB(fileUploadInfo);
String fileName = files.getUrl().substring(files.getUrl().lastIndexOf("/") + 1);
return minioUtils.getUploadObjectUrl(fileName, bucketName);
}
// 分片上传
else {
log.info("tip message: 当前分片数量 <{}> 进行分片上传", fileUploadInfo.getChunkNum());
Map<String, Object> map = minioUtils.initMultiPartUpload(fileUploadInfo, fileUploadInfo.getFileName(), fileUploadInfo.getChunkNum(), fileUploadInfo.getContentType(), bucketName);
String uploadId = (String) map.get("uploadId");
fileUploadInfo.setUploadId(uploadId);
redisUtil.set(fileUploadInfo.getFileMd5(), fileUploadInfo, breakpointTime * 60 * 60 * 24);
return map;
}
}
/**
* 文件合并
*
* @param
* @return String
*/
@Override
public String mergeMultipartUpload(FileUploadInfo fileUploadInfo) {
log.info("tip message: 通过 <{}> 开始合并<分片上传>任务", fileUploadInfo);
FileUploadInfo redisFileUploadInfo = (FileUploadInfo) redisUtil.get(fileUploadInfo.getFileMd5());
if (redisFileUploadInfo != null) {
fileUploadInfo.setFileName(redisFileUploadInfo.getFileName());
}
boolean result = minioUtils.mergeMultipartUpload(fileUploadInfo.getFileName(), fileUploadInfo.getUploadId(), fileUploadInfo.getFileType());
//合并成功
if (result) {
//存入数据库
Files files = saveFileToDB(fileUploadInfo);
redisUtil.del(fileUploadInfo.getFileMd5());
return files.getUrl();
}
return null;
}
@Override
public String getFliePath(String bucketName, String fileName) {
return minioUtils.getFliePath(bucketName, fileName);
}
@Override
public String upload(MultipartFile file, String bucketName) {
minioUtils.upload(file, bucketName);
return getFliePath(bucketName, file.getName());
}
private Files saveFileToDB(FileUploadInfo fileUploadInfo) {
String suffix = fileUploadInfo.getFileName().substring(fileUploadInfo.getFileName().lastIndexOf("."));
String url = this.getFliePath(fileUploadInfo.getFileType().toLowerCase(), fileUploadInfo.getFileMd5() + suffix);
//存入数据库
Files files = new Files();
BeanUtils.copyProperties(fileUploadInfo, files);
files.setBucketName(fileUploadInfo.getFileType());
files.setUrl(url);
filesMapper.insert(files);
return files;
}
}

View File

@ -0,0 +1,306 @@
package com.mmg.utils;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.StrUtil;
import com.google.common.collect.HashMultimap;
import com.mmg.config.CustomMinioClient;
import com.mmg.model.vo.FileUploadInfo;
import io.minio.*;
import io.minio.http.Method;
import io.minio.messages.Part;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Slf4j
@Component
public class MinioUtils {
@Value(value = "${minio.endpoint}")
private String endpoint;
@Value(value = "${minio.accesskey}")
private String accesskey;
@Value(value = "${minio.secretkey}")
private String secretkey;
@Value(value = "${minio.expiry}")
private Integer expiry;
private CustomMinioClient customMinioClient;
/**
* 用spring的自动注入会注入失败
*/
@PostConstruct
public void init() {
MinioClient minioClient = MinioClient.builder()
.endpoint(endpoint)
.credentials(accesskey, secretkey)
.build();
customMinioClient = new CustomMinioClient(minioClient);
}
/**
* 单文件签名上传
*
* @param objectName 文件全路径名称
* @param bucketName 桶名称
* @return /
*/
public Map<String, Object> getUploadObjectUrl(String objectName, String bucketName) {
try {
log.info("tip message: 通过 <{}-{}> 开始单文件上传<minio>", objectName, bucketName);
Map<String, Object> resMap = new HashMap();
List<String> partList = new ArrayList<>();
String url = customMinioClient.getPresignedObjectUrl(
GetPresignedObjectUrlArgs.builder()
.method(Method.PUT)
.bucket(bucketName)
.object(objectName)
.expiry(expiry, TimeUnit.DAYS)
.build());
log.info("tip message: 单个文件上传、成功");
partList.add(url);
resMap.put("uploadId", "SingleFileUpload");
resMap.put("urlList", partList);
return resMap;
} catch (Exception e) {
log.error("error message: 单个文件上传失败、原因:", e);
// 返回 文件上传失败
return null;
}
}
/**
* 初始化分片上传
*
* @param fileUploadInfo
* @param objectName 文件全路径名称
* @param chunkNum 分片数量
* @param contentType 类型如果类型使用默认流会导致无法预览
* @param bucketName 桶名称
* @return Mono<Map < String, Object>>
*/
public Map<String, Object> initMultiPartUpload(FileUploadInfo fileUploadInfo, String objectName, int chunkNum, String contentType, String bucketName) {
log.info("tip message: 通过 <{}-{}-{}-{}> 开始初始化<分片上传>数据", objectName, chunkNum, contentType, bucketName);
Map<String, Object> resMap = new HashMap<>();
try {
if (CharSequenceUtil.isBlank(contentType)) {
contentType = "application/octet-stream";
}
HashMultimap<String, String> headers = HashMultimap.create();
headers.put("Content-Type", contentType);
//获取uploadId
String uploadId = null;
if (StringUtils.isEmpty(fileUploadInfo.getUploadId())) {
uploadId = customMinioClient.initMultiPartUpload(bucketName, null, objectName, headers, null);
} else {
uploadId = fileUploadInfo.getUploadId();
}
resMap.put("uploadId", uploadId);
fileUploadInfo.setUploadId(uploadId);
fileUploadInfo.setChunkNum(chunkNum);
List<String> partList = new ArrayList<>();
Map<String, String> reqParams = new HashMap<>();
reqParams.put("uploadId", uploadId);
for (int i = 1; i <= chunkNum; i++) {
reqParams.put("partNumber", String.valueOf(i));
String uploadUrl = customMinioClient.getPresignedObjectUrl(
GetPresignedObjectUrlArgs.builder()
.method(Method.PUT)
.bucket(bucketName)
.object(objectName)
.expiry(1, TimeUnit.DAYS)
.extraQueryParams(reqParams)
.build());
partList.add(uploadUrl);
}
log.info("tip message: 文件初始化<分片上传>、成功");
resMap.put("urlList", partList);
return resMap;
} catch (Exception e) {
log.error("error message: 初始化分片上传失败、原因:", e);
// 返回 文件上传失败
return R.error(RespEnum.UPLOAD_FILE_FAILED);
}
}
/**
* 分片上传完后合并
*
* @param objectName 文件全路径名称
* @param uploadId 返回的uploadId
* @param bucketName 桶名称
* @return boolean
*/
public boolean mergeMultipartUpload(String objectName, String uploadId, String bucketName) {
try {
log.info("tip message: 通过 <{}-{}-{}> 合并<分片上传>数据", objectName, uploadId, bucketName);
//目前仅做了最大1000分片
Part[] parts = new Part[1000];
// 查询上传后的分片数据
ListPartsResponse partResult = customMinioClient.listMultipart(bucketName, null, objectName, 1000, 0, uploadId, null, null);
int partNumber = 1;
for (Part part : partResult.result().partList()) {
parts[partNumber - 1] = new Part(partNumber, part.etag());
partNumber++;
}
// 合并分片
customMinioClient.mergeMultipartUpload(bucketName, null, objectName, uploadId, parts, null, null);
} catch (Exception e) {
log.error("error message: 合并失败、原因:", e);
//TODO删除redis的数据
return false;
}
return true;
}
/**
* 通过 sha256 获取上传中的分片信息
*
* @param objectName 文件全路径名称
* @param uploadId 返回的uploadId
* @param bucketName 桶名称
* @return Mono<Map < String, Object>>
*/
public List<Integer> getChunkByFileMD5(String objectName, String uploadId, String bucketName) {
log.info("通过 <{}-{}-{}> 查询<minio>上传分片数据", objectName, uploadId, bucketName);
try {
// 查询上传后的分片数据
ListPartsResponse partResult = customMinioClient.listMultipart(bucketName, null, objectName, 1000, 0, uploadId, null, null);
return partResult.result().partList().stream().map(Part::partNumber).collect(Collectors.toList());
} catch (Exception e) {
log.error("error message: 查询上传后的分片信息失败、原因:", e);
return null;
}
}
/**
* 获取文件下载地址
*
* @param bucketName 桶名称
* @param fileName 文件名
* @return
*/
public String getFliePath(String bucketName, String fileName) {
return StrUtil.format("{}/{}/{}", endpoint, bucketName, fileName);//文件访问路径
}
/**
* 创建一个桶
*
* @return
*/
public String createBucket(String bucketName) {
try {
BucketExistsArgs bucketExistsArgs = BucketExistsArgs.builder().bucket(bucketName).build();
//如果桶存在
if (customMinioClient.bucketExists(bucketExistsArgs)) {
return bucketName;
}
MakeBucketArgs makeBucketArgs = MakeBucketArgs.builder().bucket(bucketName).build();
customMinioClient.makeBucket(makeBucketArgs);
return bucketName;
} catch (Exception e) {
log.error("创建桶失败:{}", e.getMessage());
throw new RuntimeException(e);
}
}
/**
* 根据文件类型获取minio桶名称
*
* @param fileType
* @return
*/
public String getBucketName(String fileType) {
try {
//String bucketName = getProperty(fileType.toLowerCase());
if (fileType != null && !fileType.equals("")) {
//判断桶是否存在
String bucketName2 = createBucket(fileType.toLowerCase());
if (bucketName2 != null && !bucketName2.equals("")) {
return bucketName2;
} else {
return fileType;
}
}
} catch (Exception e) {
log.error("Error reading bucket name ");
}
return fileType;
}
/**
* 读取配置文件
*
* @param fileType
* @return
* @throws IOException
*/
private String getProperty(String fileType) throws IOException {
Properties SysLocalPropObject = new Properties();
//判断桶关系配置文件是否为空
if (SysLocalPropObject.isEmpty()) {
InputStream is = getClass().getResourceAsStream("/BucketRelation.properties");
SysLocalPropObject.load(is);
is.close();
}
return SysLocalPropObject.getProperty("bucket." + fileType);
}
/**
* 文件上传
*
* @param file 文件
* @return Boolean
*/
public String upload(MultipartFile file, String bucketName) {
String originalFilename = file.getOriginalFilename();
if (StringUtils.isEmpty(originalFilename)) {
throw new RuntimeException();
}
String objectName = file.getName();
try {
PutObjectArgs objectArgs = PutObjectArgs.builder().bucket(bucketName).object(objectName)
.stream(file.getInputStream(), file.getSize(), -1).contentType(file.getContentType()).build();
//文件名称相同会覆盖
customMinioClient.putObject(objectArgs);
} catch (Exception e) {
e.printStackTrace();
return null;
}
// 查看文件地址
GetPresignedObjectUrlArgs build = new GetPresignedObjectUrlArgs().builder().bucket(bucketName).object(objectName).method(Method.GET).build();
String url = null;
try {
url = customMinioClient.getPresignedObjectUrl(build);
} catch (Exception e) {
e.printStackTrace();
}
return url;
}
}

View File

@ -0,0 +1,82 @@
package com.mmg.utils;
import com.alibaba.fastjson.JSON;
import java.util.HashMap;
import java.util.Map;
/**
* 返回数据
*
* @author Mark sunlightcs@gmail.com
*/
public class R extends HashMap<String, Object> {
private static final long serialVersionUID = 1L;
public R setData(Object data) {
put("data",data);
return this;
}
//利用fastjson进行反序列化
public <T> T getData(Class<T> typeReference) {
Object data = get("data"); //默认是map
String jsonString = JSON.toJSONString(data);
T t = JSON.parseObject(jsonString, typeReference);
return t;
}
public R() {
put("code", 200);
put("msg", "success");
}
public static R error() {
return error(5000, "未知异常,请联系管理员");
}
public static R error(String msg) {
return error(500, msg);
}
public static R error(RespEnum respEnum) {
return error(respEnum.getCode(), respEnum.getMessage());
}
public static R error(int code, String msg) {
R r = new R();
r.put("code", code);
r.put("msg", msg);
return r;
}
public static R ok(RespEnum respEnum) {
return ok(respEnum.getCode(), respEnum.getMessage());
}
public static R ok(String msg) {
R r = new R();
r.put("msg", msg);
return r;
}
public static R ok(int code, String msg) {
R r = new R();
r.put("code", code);
r.put("msg", msg);
return r;
}
public static R ok(Map<String, Object> map) {
R r = new R();
r.putAll(map);
return r;
}
public static R ok() {
return new R();
}
public R put(String key, Object value) {
super.put(key, value);
return this;
}
public Integer getCode() {
return (Integer) this.get("code");
}
}

View File

@ -0,0 +1,509 @@
package com.mmg.utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@Component
public class RedisUtil {
@Autowired
private RedisTemplate redisTemplate;
/**
* 指定缓存失效时间
* @param key
* @param time 时间()
* @return
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据key 获取过期时间
* @param key 不能为null
* @return 时间() 返回0代表为永久有效
*/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断key是否存在
* @param key
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除缓存
* @param key 可以传一个值 或多个
*/
@SuppressWarnings("unchecked")
public void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete((Collection<String>) CollectionUtils.arrayToList(key));
}
}
}
// ============================String=============================
/**
* 普通缓存获取
* @param key
* @return
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
* @param key
* @param value
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入并设置时间
* @param key
* @param value
* @param time 时间() time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 递增
* @param key
* @param delta 要增加几(大于0)
* @return
*/
public long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减
* @param key
* @param delta 要减少几(小于0)
* @return
*/
public long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
// ================================Map=================================
/**
* 获取list缓存的长度
* @param key
* @return
*/
public long hGetMapSize(String key) {
try {
return redisTemplate.opsForHash().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* HashGet
* @param key 不能为null
* @param item 不能为null
* @return
*/
public Object hget(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}
/**
* 获取hashKey对应的所有键值
* @param key
* @return 对应的多个键值
*/
public Map<Object, Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* HashSet
* @param key
* @param map 对应多个键值
* @return true 成功 false 失败
*/
public boolean hmset(String key, Map<String, Object> map) {
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* HashSet 并设置时间
* @param key
* @param map 对应多个键值
* @param time 时间()
* @return true成功 false失败
*/
public boolean hmset(String key, Map<String, Object> map, long time) {
try {
redisTemplate.opsForHash().putAll(key, map);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
* @param key
* @param item
* @param value
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value) {
try {
redisTemplate.opsForHash().put(key, item, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
* @param key
* @param item
* @param value
* @param time 时间() 注意:如果已存在的hash表有时间,这里将会替换原有的时间
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value, long time) {
try {
redisTemplate.opsForHash().put(key, item, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除hash表中的值
* @param key 不能为null
* @param item 可以使多个 不能为null
*/
public void hdel(String key, Object... item) {
redisTemplate.opsForHash().delete(key, item);
}
/**
* 判断hash表中是否有该项的值
* @param key 不能为null
* @param item 不能为null
* @return true 存在 false不存在
*/
public boolean hHasKey(String key, String item) {
return redisTemplate.opsForHash().hasKey(key, item);
}
/**
* hash递增 如果不存在,就会创建一个 并把新增后的值返回
* @param key
* @param item
* @param by 要增加几(大于0)
* @return
*/
public double hincr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, by);
}
/**
* hash递减
* @param key
* @param item
* @param by 要减少记(小于0)
* @return
*/
public double hdecr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, -by);
}
// ============================set=============================
/**
* 根据key获取Set中的所有值
* @param key
* @return
*/
public Set<Object> sGet(String key) {
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 根据value从一个set中查询,是否存在
* @param key
* @param value
* @return true 存在 false不存在
*/
public boolean sHasKey(String key, Object value) {
try {
return redisTemplate.opsForSet().isMember(key, value);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将数据放入set缓存
* @param key
* @param values 可以是多个
* @return 成功个数
*/
public long sSet(String key, Object... values) {
try {
return redisTemplate.opsForSet().add(key, values);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 将set数据放入缓存
* @param key
* @param time 时间()
* @param values 可以是多个
* @return 成功个数
*/
public long sSetAndTime(String key, long time, Object... values) {
try {
Long count = redisTemplate.opsForSet().add(key, values);
if (time > 0) {
expire(key, time);
}
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 获取set缓存的长度
* @param key
* @return
*/
public long sGetSetSize(String key) {
try {
return redisTemplate.opsForSet().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 移除值为value的
* @param key
* @param values 可以是多个
* @return 移除的个数
*/
public long setRemove(String key, Object... values) {
try {
Long count = redisTemplate.opsForSet().remove(key, values);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
// ===============================list=================================
/**
* 获取list缓存的内容
* @param key
* @param start 开始
* @param end 结束 0 -1代表所有值
* @return
*/
public List<Object> lGet(String key, long start, long end) {
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 获取list缓存的长度
* @param key
* @return
*/
public long lGetListSize(String key) {
try {
return redisTemplate.opsForList().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 通过索引 获取list中的值
* @param key
* @param index 索引 index>=0时 0 表头1 第二个元素依次类推index<0时-1表尾-2倒数第二个元素依次类推
* @return
*/
public Object lGetIndex(String key, long index) {
try {
return redisTemplate.opsForList().index(key, index);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 将list放入缓存
* @param key
* @param value
* @return
*/
public boolean lSet(String key, Object value) {
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
* @param key
* @param value
* @param time 时间()
* @return
*/
public boolean lSet(String key, Object value, long time) {
try {
redisTemplate.opsForList().rightPush(key, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
* @param key
* @param value
* @return
*/
public boolean lSet(String key, List<Object> value) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key
* @param value
* @param time 时间()
* @return
*/
public boolean lSet(String key, List<Object> value, long time) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据索引修改list中的某条数据
* @param key
* @param index 索引
* @param value
* @return
*/
public boolean lUpdateIndex(String key, long index, Object value) {
try {
redisTemplate.opsForList().set(key, index, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 移除N个值为value
* @param key
* @param count 移除多少个
* @param value
* @return 移除的个数
*/
public long lRemove(String key, long count, Object value) {
try {
Long remove = redisTemplate.opsForList().remove(key, count, value);
return remove;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
}

View File

@ -0,0 +1,28 @@
package com.mmg.utils;
public enum RespEnum {
UPLOADSUCCESSFUL(1, "上传成功"),
UPLOADING(2, "上传中"),
NOT_UPLOADED(3, "未上传"),
ACCESS_PARAMETER_INVALID(1001,"访问参数无效"),
UPLOAD_FILE_FAILED(1002,"文件上传失败"),
DATA_NOT_EXISTS(1003,"数据不存在"),
;
private final Integer code;
private final String message;
public Integer getCode() {
return code;
}
public String getMessage() {
return message;
}
RespEnum(Integer code, String message) {
this.code = code;
this.message = message;
}
}

View File

@ -0,0 +1,35 @@
server:
port: 9090
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/minio_upload_file?serverTimezone=Asia/Shanghai&userUnicode=true&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: 123456
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
data:
redis:
host: 127.0.0.1
port: 6379
database: 0
#password: 123456
mybatis-plus:
mapper-locations: classpath:mapper/*.xml
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
minio:
endpoint: http://47.103.114.59:9000
accesskey: OliveSmart
secretkey: OliveSmartTmzl
bucket: playedu
expiry: 1 #分片对象过期时间 单位(天)
breakpoint-time: 1 #断点续传有效时间在redis存储任务的时间 单位(天)
logging:
level:
com.mmg: debug
org.springframework: warn

View File

@ -0,0 +1,83 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<!-- 日志存放路径 -->
<property name="log.path" value="logs/" />
<!-- 日志输出格式 -->
<property name="log.pattern" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" />
<!-- 控制台输出 -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
<!-- 彩色日志 -->
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>
%d{yyyy-MM-dd HH:mm:ss} [%thread] %magenta(%-5level) %green([%-50.50class]) >>> %cyan(%msg) %n
</pattern>
</layout>
</appender>
<!-- 系统日志输出 -->
<appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/info.log</file>
<!-- 循环政策:基于时间创建日志文件 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 日志文件名格式 -->
<fileNamePattern>${log.path}/info.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 日志最大的历史 60天 -->
<maxHistory>60</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!-- 过滤的级别 -->
<level>INFO</level>
<!-- 匹配时的操作:接收(记录) -->
<onMatch>ACCEPT</onMatch>
<!-- 不匹配时的操作:拒绝(不记录) -->
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/error.log</file>
<!-- 循环政策:基于时间创建日志文件 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 日志文件名格式 -->
<fileNamePattern>${log.path}/error.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 日志最大的历史 60天 -->
<maxHistory>60</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!-- 过滤的级别 -->
<level>ERROR</level>
<!-- 匹配时的操作:接收(记录) -->
<onMatch>ACCEPT</onMatch>
<!-- 不匹配时的操作:拒绝(不记录) -->
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 系统模块日志级别控制 -->
<logger name="com.zsincere" level="info" />
<!-- Spring日志级别控制 -->
<logger name="org.springframework" level="warn" />
<!-- zsincere-mq 日志级别控制 -->
<logger name="com.zsincere.mq.common" level="info" />
<root level="info">
<appender-ref ref="console" />
</root>
<!--系统操作日志-->
<root level="info">
<appender-ref ref="file_info" />
<appender-ref ref="file_error" />
</root>
</configuration>

View File

@ -0,0 +1,13 @@
package com.mmg;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class MinioFileUploadApplicationTests {
@Test
void contextLoads() {
}
}

30
minio-fornt/.gitignore vendored Normal file
View File

@ -0,0 +1,30 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo

3
minio-fornt/.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

29
minio-fornt/README.md Normal file
View File

@ -0,0 +1,29 @@
# minio-fornt
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
## Customize configuration
See [Vite Configuration Reference](https://vitejs.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Compile and Minify for Production
```sh
npm run build
```

13
minio-fornt/index.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

View File

@ -0,0 +1,8 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["node_modules", "dist"]
}

2064
minio-fornt/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

22
minio-fornt/package.json Normal file
View File

@ -0,0 +1,22 @@
{
"name": "minio-fornt",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.7.2",
"element-plus": "^2.7.4",
"spark-md5": "^3.0.2",
"vue": "^3.4.21",
"vue-router": "^4.3.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.4",
"vite": "^5.2.8"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

10
minio-fornt/src/App.vue Normal file
View File

@ -0,0 +1,10 @@
<script setup>
import {RouterLink, RouterView} from 'vue-router'
</script>
<template>
<RouterView/>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,49 @@
import request from '@/utils/request'
//上传信息
export function uploadScreenshot(data){
return request({
url:'upload/multipart/uploadScreenshot',
method:'post',
data
})
}
//上传信息
export function uploadFileInfo(data){
return request({
url:'upload/multipart/uploadFileInfo',
method:'post',
data
})
}
// 上传校验
export function checkUpload(MD5) {
return request({
url: `upload/multipart/check?md5=${MD5}`,
method: 'get',
})
};
// 初始化上传
export function initUpload(data) {
return request({
url: `upload/multipart/init`,
method: 'post',
data
})
};
// 初始化上传
export function mergeUpload(data) {
return request({
url: `upload/multipart/merge`,
method: 'post',
data
})
};

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

After

Width:  |  Height:  |  Size: 276 B

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
/>
</svg>
</template>

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
<path
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
/>
</svg>
</template>

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
<path
d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"
/>
</svg>
</template>

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
/>
</svg>
</template>

View File

@ -0,0 +1,19 @@
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
aria-hidden="true"
role="img"
class="iconify iconify--mdi"
width="24"
height="24"
preserveAspectRatio="xMidYMid meet"
viewBox="0 0 24 24"
>
<path
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
fill="currentColor"
></path>
</svg>
</template>

13
minio-fornt/src/main.js Normal file
View File

@ -0,0 +1,13 @@
import {createApp} from 'vue'
import App from './App.vue'
import router from './router'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
const app = createApp(App)
app.use(router)
app.use(ElementPlus)
app.mount('#app')

View File

@ -0,0 +1,15 @@
import { createRouter, createWebHistory } from 'vue-router'
import FileView from "@/views/FileView.vue";
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: FileView
},
]
})
export default router

View File

@ -0,0 +1,109 @@
/**
* @param: fileName - 文件名称
* @param: 数据返回 1) 无后缀匹配 - false
* @param: 数据返回 2) 匹配图片 - image
* @param: 数据返回 3) 匹配 txt - txt
* @param: 数据返回 4) 匹配 excel - excel
* @param: 数据返回 5) 匹配 word - word
* @param: 数据返回 6) 匹配 pdf - pdf
* @param: 数据返回 7) 匹配 ppt - ppt
* @param: 数据返回 8) 匹配 视频 - video
* @param: 数据返回 9) 匹配 音频 - radio
* @param: 数据返回 10) 其他匹配项 - other
* @author: ljw
**/
export function fileSuffixTypeUtil(fileName){
// 后缀获取
var suffix = "";
// 获取类型结果
var result = "";
try {
var flieArr = fileName.split(".");
suffix = flieArr[flieArr.length - 1];
} catch (err) {
suffix = "";
}
// fileName无后缀返回 false
if (!suffix) {
result = false;
return result;
}
// 图片格式
var imglist = ["png", "jpg", "jpeg", "bmp", "gif"];
// 进行图片匹配
result = imglist.some(function (item) {
return item == suffix;
});
if (result) {
result = "image";
return result;
}
// 匹配txt
var txtlist = ["txt"];
result = txtlist.some(function (item) {
return item == suffix;
});
if (result) {
result = "txt";
return result;
}
// 匹配 excel
var excelist = ["xls", "xlsx"];
result = excelist.some(function (item) {
return item == suffix;
});
if (result) {
result = "excel";
return result;
}
// 匹配 word
var wordlist = ["doc", "docx"];
result = wordlist.some(function (item) {
return item == suffix;
});
if (result) {
result = "word";
return result;
}
// 匹配 pdf
var pdflist = ["pdf"];
result = pdflist.some(function (item) {
return item == suffix;
});
if (result) {
result = "pdf";
return result;
}
// 匹配 ppt
var pptlist = ["ppt"];
result = pptlist.some(function (item) {
return item == suffix;
});
if (result) {
result = "ppt";
return result;
}
// 匹配 视频
var videolist = ["mp4", "m2v", "mkv","ogg", "flv", "avi", "wmv", "rmvb"];
result = videolist.some(function (item) {
return item == suffix;
});
if (result) {
result = "video";
return result;
}
// 匹配 音频
var radiolist = ["mp3", "wav", "wmv"];
result = radiolist.some(function (item) {
return item == suffix;
});
if (result) {
result = "radio";
return result;
}
// 其他 文件类型
result = "other";
return result;
};

View File

@ -0,0 +1,42 @@
import axios from 'axios'
const request = axios.create({
baseURL: `http://localhost:9090`,
timeout: 30000
})
// request 拦截器
// 可以自请求发送前对请求做一些处理
// 比如统一加token对请求参数统一加密
request.interceptors.request.use(config => {
config.headers['Content-Type'] = 'application/json;charset=utf-8';
return config
}, error => {
return Promise.reject(error)
});
// response 拦截器
// 可以在接口响应后统一处理结果
request.interceptors.response.use(
response => {
let res = response.data;
// 如果是返回的文件
if (response.headers === 'blob') {
return res
}
// 兼容服务端返回的字符串数据
if (typeof res === 'string') {
res = res ? JSON.parse(res) : res
console.log(res)
}
return res;
},
error => {
console.log('err' + error) // for debug
return Promise.reject(error)
}
)
export default request

View File

@ -0,0 +1,338 @@
<template>
<div class="container">
<div style="display:none;">
<video width="500" height="240" controls id="upvideo">
</video>
</div>
<h2>上传示例</h2>
<div class="upload-demo">
<el-upload ref="upload" action="https://jsonplaceholder.typicode.com/posts/"
:on-remove="handleRemove" :on-change="handleFileChange" :file-list="data.uploadFileList"
:show-file-list="false"
:auto-upload="false" multiple>
<el-button slot="trigger" type="primary" plain>选择文件</el-button>
</el-upload>
<el-button style="margin: 5px;" type="success" @click="handler">上传</el-button>
<el-button type="danger" @click="clearFileHandler">清空</el-button>
</div>
<table style="margin-top: 20px">
<th>
文件名
</th>
<th>
文件大小
</th>
<th>
上传进度
</th>
<th>
状态
</th>
</table>
<!-- 文件列表 -->
<div class="file-list-wrapper">
<el-collapse>
<el-collapse-item v-for="item in data.uploadFileList">
<template #title>
<div class="upload-file-item">
<div class="file-info-item file-name" :title="item.name">{{ item.name }}</div>
<div class="file-info-item file-size">{{ item.size }}</div>
<div class="file-info-item file-progress">
<span class="file-progress-label"></span>
<el-progress :percentage="item.uploadProgress" class="file-progress-value"/>
</div>
<div class="file-info-item file-size"><span></span>
<el-tag v-if="item.status === '等待上传'" size="small" type="info">等待上传</el-tag>
<el-tag v-else-if="item.status === '校验MD5'" size="small" type="warning">校验MD5</el-tag>
<el-tag v-else-if="item.status === '正在上传'" size="small">正在上传</el-tag>
<el-tag v-else-if="item.status === '上传成功'" size="small" type="success">上传完成</el-tag>
<el-tag v-else size="small">正在上传</el-tag>
<!-- <el-tag v-else size="medium" type="danger">上传错误</el-tag>-->
</div>
</div>
</template>
<div class="file-chunk-list-wrapper">
<!-- 分片列表 -->
<el-table :data="item.chunkList" max-height="400" style="width: 100%">
<el-table-column prop="chunkNumber" label="分片序号" width="180">
</el-table-column>
<el-table-column prop="progress" label="上传进度">
<template v-slot="{ row }">
<el-progress v-if="!row.status || row.progressStatus === 'normal'"
:percentage="row.progress"/>
<el-progress v-else :percentage="row.progress" :status="row.progressStatus"
:text-inside="true" :stroke-width="16"/>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="180">
</el-table-column>
</el-table>
</div>
</el-collapse-item>
</el-collapse>
</div>
</div>
</template>
<script setup>
import {ref, reactive} from 'vue';
import {checkUpload, initUpload, mergeUpload, uploadFileInfo} from '@/api/upload';
import {fileSuffixTypeUtil} from '@/utils/FileUtil';
import axios from 'axios';
import SparkMD5 from 'spark-md5';
import {ElMessageBox} from "element-plus";
const FILE_UPLOAD_ID_KEY = 'file_upload_id';
const chunkSize = 10 * 1024 * 1024; // 10MB
let currentFileIndex = 0;
const FileStatus = {
wait: '等待上传',
getMd5: '校验MD5',
chip: '正在创建序列',
uploading: '正在上传',
success: '上传成功',
error: '上传错误'
};
const simultaneousUploads = ref(3);
const data = reactive({
uploadFileList: []
});
const handler = () => {
if (data.uploadFileList.length === 0) {
ElMessageBox.alert('请先选择文件')
return false;
}
if (currentFileIndex >= data.uploadFileList.length) {
ElMessageBox.alert('文件上传完成')
return false;
}
const currentFile = data.uploadFileList[currentFileIndex];
currentFile.status = FileStatus.getMd5;
currentFile.chunkUploadedList = [];
getFileMd5(currentFile.raw, async (md5, totalChunks) => {
const checkResult = await checkFileUploadedByMd5(md5);
if (checkResult.code === 1) {
currentFile.status = FileStatus.success;
currentFile.uploadProgress = 100;
currentFileIndex++;
handler();
return;
} else if (checkResult.code === 2) {
currentFile.status = FileStatus.uploading;
currentFile.chunkUploadedList = checkResult.data.chunkUploadedList;
} else {
console.log('未上传');
}
currentFile.status = FileStatus.chip;
let fileChunks = createFileChunk(currentFile.raw, chunkSize);
let type = fileSuffixTypeUtil(currentFile.name);
let param = {
fileName: currentFile.name,
fileSize: currentFile.size,
chunkSize: chunkSize,
chunkNum: totalChunks,
fileMd5: md5,
contentType: 'application/octet-stream',
fileType: type,
chunkUploadedList: currentFile.chunkUploadedList
};
let uploadIdInfoResult = await getFileUploadUrls(param);
let uploadIdInfo = uploadIdInfoResult.data;
let uploadUrls = uploadIdInfo.urlList;
currentFile.chunkList = [];
if (uploadUrls && fileChunks.length !== uploadUrls.length) {
await ElMessageBox.alert('文件上传完成')
return;
}
fileChunks.map((chunkItem, index) => {
if (currentFile.chunkUploadedList.indexOf(index + 1) !== -1) {
currentFile.chunkList.push({
chunkNumber: index + 1,
chunk: chunkItem,
uploadUrl: uploadUrls[index],
progress: 100,
progressStatus: 'success',
status: '上传成功'
});
} else {
currentFile.chunkList.push({
chunkNumber: index + 1,
chunk: chunkItem,
uploadUrl: uploadUrls[index],
progress: 0,
status: '—'
});
}
});
let tempFileChunks = [];
currentFile.chunkList.forEach((item) => {
tempFileChunks.push(item);
});
currentFile.status = FileStatus.uploading;
tempFileChunks = processUploadChunkList(tempFileChunks);
await uploadChunkBase(tempFileChunks);
if (uploadIdInfo.uploadId === "SingleFileUpload") {
currentFile.status = FileStatus.success;
currentFileIndex++;
handler();
return;
} else {
const mergeResult = await mergeFile({
uploadId: uploadIdInfo.uploadId,
fileName: currentFile.name,
fileMd5: md5,
fileType: type,
chunkNum: uploadIdInfo.urlList.length,
chunkSize: chunkSize,
fileSize: currentFile.size
});
if (!mergeResult.data) {
currentFile.status = FileStatus.error;
this.$message.error(mergeResult.error);
} else {
localStorage.removeItem(FILE_UPLOAD_ID_KEY);
currentFile.status = FileStatus.success;
currentFileIndex++;
handler();
}
}
});
};
const clearFileHandler = () => {
data.uploadFileList.splice(0, data.uploadFileList.length);
currentFileIndex = 0;
};
const handleFileChange = (file, fileList) => {
initFileProperties(file);
data.uploadFileList.splice(0, data.uploadFileList.length, ...fileList);
console.log("data.uploadFileList", data.uploadFileList)
};
const initFileProperties = (file) => {
file.chunkList = [];
file.status = FileStatus.wait;
file.progressStatus = 'warning';
file.uploadProgress = 0;
};
const handleRemove = (file, fileList) => {
data.uploadFileList.splice(0, data.uploadFileList.length, ...fileList);
};
const getFileMd5 = (file, callback) => {
let fileReader = new FileReader();
fileReader.readAsArrayBuffer(file);
fileReader.onload = function (e) {
let spark = new SparkMD5.ArrayBuffer();
spark.append(e.target.result);
callback(spark.end(), Math.ceil(file.size / chunkSize));
};
};
const createFileChunk = (file, size) => {
let fileChunks = [];
let cur = 0;
while (cur < file.size) {
fileChunks.push({file: file.slice(cur, cur + size)});
cur += size;
}
return fileChunks;
};
const checkFileUploadedByMd5 = async (md5) => {
const response = await checkUpload(md5);
return response;
};
const getFileUploadUrls = async (param) => {
const response = await initUpload(param);
return response;
};
const processUploadChunkList = (chunkList) => {
const temp = [];
chunkList.forEach((chunk) => {
temp.push(chunk);
});
return temp;
};
const uploadChunkBase = async (chunkList) => {
const uploadPromiseList = [];
const limit = simultaneousUploads.value;
for (let i = 0; i < chunkList.length; i++) {
if (chunkList[i].progress !== 100) {
chunkList[i].status = FileStatus.uploading;
let params = chunkList[i];
uploadPromiseList.push(
uploadFileChunk(params)
.then(() => {
chunkList[i].progress = 100;
chunkList[i].progressStatus = 'success';
chunkList[i].status = '上传成功';
})
.catch(() => {
chunkList[i].progress = 100;
chunkList[i].progressStatus = 'exception';
chunkList[i].status = '上传失败';
})
);
}
if (uploadPromiseList.length === limit || i === chunkList.length - 1) {
await Promise.all(uploadPromiseList);
}
}
};
const uploadFileChunk = async (chunk) => {
let formData = new FormData();
formData.append('file', chunk.chunk.file);
await axios.put(chunk.uploadUrl, formData, {
headers: {'Content-Type': 'application/octet-stream'}
});
};
</script>
<style scoped>
.container {
padding: 20px;
}
.file-list-wrapper {
margin-top: 20px;
}
.upload-file-item {
display: flex;
align-items: center;
}
.file-info-item {
flex: 1;
text-align: center;
}
.file-name {
text-align: left;
padding-left: 20px;
}
.file-progress {
width: 200px;
margin: 0 20px;
}
</style>

View File

@ -0,0 +1,16 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})

42
sql/minio_upload_file.sql Normal file
View File

@ -0,0 +1,42 @@
/*
Navicat Premium Data Transfer
Source Server : Local
Source Server Type : MySQL
Source Server Version : 80027 (8.0.27)
Source Host : 127.0.0.1:3306
Source Schema : minio_upload_file
Target Server Type : MySQL
Target Server Version : 80027 (8.0.27)
File Encoding : 65001
Date: 04/06/2024 13:41:50
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for files
-- ----------------------------
DROP TABLE IF EXISTS `files`;
CREATE TABLE `files` (
`id` int NOT NULL AUTO_INCREMENT COMMENT 'id',
`upload_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '分片上传uploadId',
`file_md5` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '文件md5',
`url` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '下载链接',
`file_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '文件名称',
`bucket_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '桶名',
`file_type` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '文件类型',
`file_size` bigint NULL DEFAULT NULL COMMENT '文件大小(byte)',
`chunk_size` bigint NULL DEFAULT NULL COMMENT '每个分片的大小byte',
`chunk_num` int NULL DEFAULT NULL COMMENT '分片数量',
`is_delete` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除',
`enable` tinyint(1) NULL DEFAULT 1 COMMENT '是否禁用链接(0 禁用 1启用)',
`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
`update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 309 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '文件表' ROW_FORMAT = DYNAMIC;
SET FOREIGN_KEY_CHECKS = 1;