MongoDB文件存储服务

This commit is contained in:
jiang 2024-06-21 13:17:46 +08:00
parent ea9b998c46
commit b5f03d1c1a
26 changed files with 853 additions and 0 deletions

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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.bonus</groupId>
<artifactId>bonus-modules</artifactId>
<version>3.6.4</version>
</parent>
<groupId>com.bonus.mongoDB</groupId>
<artifactId>bonus-modules-mongodb</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- SpringCloud Alibaba Nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- SpringCloud Alibaba Nacos Config -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- SpringCloud Alibaba Sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!-- SpringBoot Actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- FastDFS -->
<dependency>
<groupId>com.github.tobato</groupId>
<artifactId>fastdfs-client</artifactId>
</dependency>
<!-- Minio -->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>${minio.version}</version>
</dependency>
<!-- bonus Api System -->
<dependency>
<groupId>com.bonus</groupId>
<artifactId>bonus-api-system</artifactId>
</dependency>
<!-- bonus Common Swagger -->
<dependency>
<groupId>com.bonus</groupId>
<artifactId>bonus-common-swagger</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.12</version> <!-- 版本号可以根据需要调整 -->
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,25 @@
package com.bonus.mongodb;
import com.bonus.common.swagger.annotation.EnableCustomSwagger2;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
@EnableCustomSwagger2
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class })
public class BonusMongodbApplication {
public static void main(String[] args)
{
SpringApplication.run(BonusMongodbApplication.class, args);
System.out.println("(♥◠‿◠)ノ゙ MongoDB存储服务模块启动成功 ლ(´ڡ`ლ)゙ \n" +
" .-------. ____ __ \n" +
" | _ _ \\ \\ \\ / / \n" +
" | ( ' ) | \\ _. / ' \n" +
" |(_ o _) / _( )_ .' \n" +
" | (_,_).' __ ___(_ o _)' \n" +
" | |\\ \\ | || |(_,_)' \n" +
" | | \\ `' /| `-' / \n" +
" | | \\ / \\ / \n" +
" ''-' `'-' `-..-' ");
}
}

View File

@ -0,0 +1,63 @@
package com.bonus.mongodb.config;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.gridfs.GridFSBucket;
import com.mongodb.client.gridfs.GridFSBuckets;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.net.URLEncoder;
/**
* @author coisini
* @version 1.0
* @Description MongoDB配置类
* @date Apr 17, 2022
*/
@Configuration
@ConfigurationProperties(prefix = "spring.data.mongodb")
@Data
public class MongoConfig {
/**
* 数据库配置信息
*/
private String database;
private String host;
private Integer port;
private String username;
private String password;
// 移除静态修饰符避免单例模式
private MongoClient mongoClient;
// 使用@Bean注解并在方法内部进行异常处理
@Bean
public MongoClient getMongoClient() {
if (mongoClient == null) {
try {
mongoClient = MongoClients.create(String.format("mongodb://%s:%s@%s:%d/%s", URLEncoder.encode(username, "UTF-8"),URLEncoder.encode(password, "UTF-8"), host, port, database));
} catch (Exception e) {
// 在实际应用中应该有更详细的异常处理策略例如记录日志并抛出自定义异常
throw new RuntimeException("Failed to create MongoClient", e);
}
}
return mongoClient;
}
// 直接返回MongoDatabase实例依赖注入会处理实例的获取
public MongoDatabase getMongoDatabase() {
return getMongoClient().getDatabase(database);
}
// 创建GridFSBucket的@Bean方法依赖注入MongoDatabase
@Bean
public GridFSBucket getGridFsBucket() {
return GridFSBuckets.create(getMongoDatabase());
}
}

View File

@ -0,0 +1,75 @@
package com.bonus.mongodb.controller;
import com.bonus.common.core.domain.R;
import com.bonus.mongodb.damain.FileExportVo;
import com.bonus.mongodb.service.MongodbService;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.formula.functions.T;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.util.Arrays;
import java.util.List;
@RestController
@RequestMapping("/mongodb/")
@Slf4j
public class MongodbController {
@Resource
private MongodbService mongodbService;
@ApiOperation(value = "单文件上传")
@PostMapping("uploadFile")
public R<FileExportVo> uploadFile(MultipartFile file) {
try {
FileExportVo fileExportVo = mongodbService.uploadFile(file);
return R.ok(fileExportVo);
} catch (Exception e) {
log.error("单文件上传", e);
return R.fail();
}
}
@ApiOperation(value = "多文件上传")
@PostMapping("uploadFiles")
public R<List<FileExportVo>> uploadFiles(MultipartFile[] files) {
try {
List<MultipartFile> multipartFiles = Arrays.asList(files);
List<FileExportVo> list = mongodbService.uploadFiles(multipartFiles);
return R.ok(list);
} catch (Exception e) {
log.error("多文件上传", e);
return R.fail();
}
}
@ApiOperation(value = "文件删除")
@PostMapping("delFile")
public R<T> delFile(String fileId) {
try {
mongodbService.removeFile(fileId);
return R.ok();
} catch (Exception e) {
log.error("文件删除", e);
return R.fail();
}
}
@ApiOperation(value = "获取文件base64")
@PostMapping("getFileBase64")
public R<byte[]> getFileBase64(String fileId) {
try {
FileExportVo fileExportVo = mongodbService.downloadFile(fileId);
return R.ok(fileExportVo.getData());
} catch (Exception e) {
log.error("获取文件base64", e);
return R.fail();
}
}
}

View File

@ -0,0 +1,44 @@
package com.bonus.mongodb.damain;
import java.io.Serializable;
import java.util.Objects;
import cn.hutool.core.bean.BeanUtil;
import lombok.Data;
@Data
public class FileExportVo implements Serializable {
private String file;
private String fileId;
private String fileName;
private String contentType;
private String suffix;
private long fileSize;
private String sourceId;
private String id;
private String fileType;
/**
* 资源类型
*/
private String sourceType;
private String updaetTime;
private byte[] data;
public FileExportVo(MongoFile mongoFile) {
BeanUtil.copyProperties(mongoFile, this);
if (Objects.nonNull(mongoFile.getContent())) {
this.data = mongoFile.getContent().getData();
}
this.fileId = mongoFile.getId();
}
}

View File

@ -0,0 +1,63 @@
package com.bonus.mongodb.damain;
import lombok.Builder;
import lombok.Data;
import org.bson.types.Binary;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
@Builder
@Data
@Document(collection = "files")
public class MongoFile {
/**
* 主键
*/
@Id
public String id;
/**
* 文件名称
*/
public String fileName;
/**
* 文件大小
*/
public long fileSize;
/**
* 上传时间
*/
public String uploadDate;
/**
* MD5值
*/
public String md5;
/**
* 文件内容
*/
private Binary content;
/**
* 文件类型
*/
public String contentType;
/**
* 文件后缀名
*/
public String suffix;
/**
* 文件描述
*/
public String description;
/**
* 大文件管理GridFS的ID
*/
private String gridFsId;
}

View File

@ -0,0 +1,8 @@
package com.bonus.mongodb.repository;
import com.bonus.mongodb.damain.MongoFile;
import org.springframework.data.mongodb.repository.MongoRepository;
public interface MongoFileRepository extends MongoRepository<MongoFile, String> {
}

View File

@ -0,0 +1,43 @@
package com.bonus.mongodb.service;
import com.bonus.mongodb.damain.FileExportVo;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
public interface MongodbService {
/**
* 文件上传
*
* @param file
* @return FileExportVo
* @throws Exception
* @description
* @date 2024/3/11 15:44
*/
FileExportVo uploadFile(MultipartFile file) throws Exception;
/**
* 多文件上传
*
* @param files
* @return
*/
List<FileExportVo> uploadFiles(List<MultipartFile> files);
/**
* 文件下载
*
* @param fileId
* @return
*/
FileExportVo downloadFile(String fileId);
/**
* 文件删除
*
* @param fileId
*/
void removeFile(String fileId);
}

View File

@ -0,0 +1,279 @@
package com.bonus.mongodb.service.impl;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.IdUtil;
import com.bonus.common.core.utils.DateUtils;
import com.bonus.mongodb.damain.FileExportVo;
import com.bonus.mongodb.damain.MongoFile;
import com.bonus.mongodb.repository.MongoFileRepository;
import com.bonus.mongodb.service.MongodbService;
import com.bonus.mongodb.utils.Md5Util;
import com.mongodb.client.gridfs.GridFSBucket;
import com.mongodb.client.gridfs.GridFSDownloadStream;
import com.mongodb.client.gridfs.model.GridFSFile;
import lombok.RequiredArgsConstructor;
import org.bson.types.Binary;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.gridfs.GridFsResource;
import org.springframework.data.mongodb.gridfs.GridFsTemplate;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.util.Objects;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class MongodbServiceImpl implements MongodbService {
private static final Logger log = LoggerFactory.getLogger(MongodbServiceImpl.class);
private final MongoFileRepository mongoFileRepository;
private final MongoTemplate mongoTemplate;
private final GridFsTemplate gridFsTemplate;
private final GridFSBucket gridFSBucket;
private static final int MAX_SIZE = 16777216;
private static final String SUFFIX = ".";
/**
* 多文件上传
*
* @param files
* @return
*/
@Override
public List<FileExportVo> uploadFiles(List<MultipartFile> files) {
return files.stream().map(file -> {
try {
return this.uploadFile(file);
} catch (Exception e) {
return null;
}
}).filter(Objects::nonNull).collect(Collectors.toList());
}
/**
* 文件上传
*
* @param file
* @return
* @throws Exception
*/
@Override
public FileExportVo uploadFile(MultipartFile file) throws Exception {
if (file.getSize() > MAX_SIZE) {
return this.saveGridFsFile(file);
} else {
return this.saveBinaryFile(file);
}
}
/**
* 文件下载
*
* @param fileId
* @return
*/
@Override
public FileExportVo downloadFile(String fileId) {
Optional<MongoFile> option = this.getBinaryFileById(fileId);
if (option.isPresent()) {
MongoFile mongoFile = option.get();
if (Objects.isNull(mongoFile.getContent())) {
option = this.getGridFsFileById(fileId);
}
}
return option.map(FileExportVo::new).orElse(null);
}
/**
* 文件删除
*
* @param fileId
*/
@Override
public void removeFile(String fileId) {
Optional<MongoFile> option = this.getBinaryFileById(fileId);
if (option.isPresent()) {
if (Objects.nonNull(option.get().getGridFsId())) {
this.removeGridFsFile(fileId);
} else {
this.removeBinaryFile(fileId);
}
}
}
/**
* 删除Binary文件
*
* @param fileId
*/
public void removeBinaryFile(String fileId) {
mongoFileRepository.deleteById(fileId);
}
/**
* 删除GridFs文件
*
* @param fileId
*/
public void removeGridFsFile(String fileId) {
// TODO 根据id查询文件
MongoFile mongoFile = mongoTemplate.findById(fileId, MongoFile.class);
if (Objects.nonNull(mongoFile)) {
// TODO 根据文件ID删除fs.files和fs.chunks中的记录
Query deleteFileQuery = new Query().addCriteria(Criteria.where("filename").is(mongoFile.getGridFsId()));
gridFsTemplate.delete(deleteFileQuery);
// TODO 删除集合mongoFile中的数据
Query deleteQuery = new Query(Criteria.where("id").is(fileId));
mongoTemplate.remove(deleteQuery, MongoFile.class);
}
}
/**
* 保存Binary文件小文件
*
* @param file
* @return
* @throws Exception
*/
public FileExportVo saveBinaryFile(MultipartFile file) throws Exception {
String suffix = getFileSuffix(file);
MongoFile mongoFile = mongoFileRepository.save(
MongoFile.builder()
.fileName(file.getOriginalFilename())
.fileSize(file.getSize())
.content(new Binary(file.getBytes()))
.contentType(file.getContentType())
.uploadDate(DateUtils.getTime())
.suffix(suffix)
.md5(Md5Util.getMd5(file.getInputStream()))
.build()
);
return new FileExportVo(mongoFile);
}
/**
* 保存GridFs文件大文件
*
* @param file
* @return
* @throws Exception
*/
public FileExportVo saveGridFsFile(MultipartFile file) throws Exception {
String suffix = getFileSuffix(file);
String gridFsId = this.storeFileToGridFs(file.getInputStream(), file.getContentType());
MongoFile mongoFile = mongoTemplate.save(
MongoFile.builder()
.fileName(file.getOriginalFilename())
.fileSize(file.getSize())
.contentType(file.getContentType())
.uploadDate(DateUtils.getTime())
.suffix(suffix)
.md5(Md5Util.getMd5(file.getInputStream()))
.gridFsId(gridFsId)
.build()
);
return new FileExportVo(mongoFile);
}
/**
* 上传文件到Mongodb的GridFs中
*
* @param in
* @param contentType
* @return
*/
public String storeFileToGridFs(InputStream in, String contentType) throws IOException {
try {
String gridFsId = IdUtil.simpleUUID();
// TODO 将文件存储进GridFS中
gridFsTemplate.store(in, gridFsId, contentType);
return gridFsId;
}catch (Exception e){
log.error("上传文件到Mongodb的GridFs中失败",e);
}
finally {
in.close();
}
return null;
}
/**
* 获取Binary文件
*
* @param id
* @return
*/
public Optional<MongoFile> getBinaryFileById(String id) {
return mongoFileRepository.findById(id);
}
/**
* 获取Grid文件
*
* @param id
* @return
*/
public Optional<MongoFile> getGridFsFileById(String id) {
MongoFile mongoFile = mongoTemplate.findById(id, MongoFile.class);
if (Objects.nonNull(mongoFile)) {
Query gridQuery = new Query().addCriteria(Criteria.where("filename").is(mongoFile.getGridFsId()));
try {
// TODO 根据id查询文件
GridFSFile fsFile = gridFsTemplate.findOne(gridQuery);
// TODO 打开流下载对象
GridFSDownloadStream in = gridFSBucket.openDownloadStream(fsFile.getObjectId());
if (in.getGridFSFile().getLength() > 0) {
// TODO 获取流对象
GridFsResource resource = new GridFsResource(fsFile, in);
// TODO 获取数据
mongoFile.setContent(new Binary(IoUtil.readBytes(resource.getInputStream())));
return Optional.of(mongoFile);
} else {
return Optional.empty();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return Optional.empty();
}
/**
* 获取文件后缀
*
* @param file
* @return
*/
private String getFileSuffix(MultipartFile file) {
String suffix = "";
if (Objects.requireNonNull(file.getOriginalFilename()).contains(SUFFIX)) {
suffix = file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf("."));
}
return suffix;
}
}

View File

@ -0,0 +1,37 @@
package com.bonus.mongodb.utils;
import java.io.IOException;
import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
* @author 10488
* @version 1.0
* @Description MD5工具类
* @date Apr 8, 2022
*/
public class Md5Util {
/**
* 获取该输入流的MD5值
*/
public static String getMd5(InputStream is) throws NoSuchAlgorithmException, IOException {
StringBuffer md5 = new StringBuffer();
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] dataBytes = new byte[1024];
int nread = 0;
while ((nread = is.read(dataBytes)) != -1) {
md.update(dataBytes, 0, nread);
}
;
byte[] mdbytes = md.digest();
// convert the byte to hex format
for (int i = 0; i < mdbytes.length; i++) {
md5.append(Integer.toString((mdbytes[i] & 0xff) + 0x100, 16).substring(1));
}
is.close();
return md5.toString();
}
}

View File

@ -0,0 +1,9 @@
Spring Boot Version: ${spring-boot.version}
Spring Application Name: ${spring.application.name}
_ _
| | | |
| |__ ___ _ __ _ _ ___ ______ ___ | |__ ___
| '_ \ / _ \ | '_ \ | | | | / __| |______| / _ \ | '_ \ / __|
| |_) | | (_) | | | | | | |_| | \__ \ | (_) | | |_) | \__ \
|_.__/ \___/ |_| |_| \__,_| |___/ \___/ |_.__/ |___/

View File

@ -0,0 +1,29 @@
# Tomcat
server:
port: 9206
# Spring
spring:
application:
# 应用名称
name: bonus-mongodb
profiles:
# 环境配置
active: dev
cloud:
nacos:
username: nacos
password: nacos
discovery:
# 服务注册地址
server-addr: 192.168.0.14:8848
namespace: f1fcd3ea-9460-4597-8acd-0f334527017c
config:
# 配置中心地址
server-addr: 192.168.0.14:8848
namespace: f1fcd3ea-9460-4597-8acd-0f334527017c
# 配置文件格式
file-extension: yml
# 共享配置
shared-configs:
- application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}

View File

@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<!-- 日志存放路径 -->
<property name="log.path" value="logs/bonus-file" />
<!-- 日志输出格式 -->
<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>
</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.bonus" level="info" />
<!-- Spring日志级别控制 -->
<logger name="org.springframework" level="warn" />
<root level="info">
<appender-ref ref="console" />
</root>
<!--系统操作日志-->
<root level="info">
<appender-ref ref="file_info" />
<appender-ref ref="file_error" />
</root>
</configuration>