Compare commits

...

2 Commits

Author SHA1 Message Date
jiang d76808d79c Merge remote-tracking branch 'origin/main' 2024-11-18 16:56:49 +08:00
jiang 905ae21929 用户登录问题修改 2024-11-18 16:56:25 +08:00
14 changed files with 1468 additions and 111 deletions

View File

@ -106,6 +106,13 @@
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb</artifactId>
</dependency>
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.5.12</version>
</dependency>
</dependencies>
<build>

View File

@ -1,5 +1,6 @@
package com.bonus.file.config;
import lombok.Data;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
@ -14,12 +15,13 @@ import io.minio.MinioClient;
@Configuration
@ConditionalOnProperty(name = "storage.type", havingValue = "minio")
@ConfigurationProperties(prefix = "minio")
@Data
public class MinioConfig
{
/**
* 服务地址
*/
private String url;
private String endpoint;
/**
* 用户名
@ -36,49 +38,9 @@ public class MinioConfig
*/
private String bucketName;
public String getUrl()
@Bean
public MinioClient getMinioClient()
{
return url;
return MinioClient.builder().endpoint(endpoint).credentials(accessKey, secretKey).build();
}
public void setUrl(String url)
{
this.url = url;
}
public String getAccessKey()
{
return accessKey;
}
public void setAccessKey(String accessKey)
{
this.accessKey = accessKey;
}
public String getSecretKey()
{
return secretKey;
}
public void setSecretKey(String secretKey)
{
this.secretKey = secretKey;
}
public String getBucketName()
{
return bucketName;
}
public void setBucketName(String bucketName)
{
this.bucketName = bucketName;
}
// @Bean
// public MinioClient getMinioClient()
// {
// return MinioClient.builder().endpoint(url).credentials(accessKey, secretKey).build();
// }
}

View File

@ -4,22 +4,23 @@ import cn.hutool.core.util.ObjectUtil;
import com.bonus.common.core.constant.HttpStatus;
import com.bonus.common.core.utils.Base64Utils;
import com.bonus.common.core.web.domain.AjaxResult;
import com.bonus.file.service.ISysFileService;
import com.bonus.system.api.domain.SysFile;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import com.bonus.common.core.domain.R;
import com.bonus.file.service.ISysFileService;
import com.bonus.system.api.domain.SysFile;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.URLDecoder;
import java.util.List;
/**
* 文件请求处理
@ -132,35 +133,37 @@ public class SysFileController
/**
* 创建文件夹
* @param folderName 单个文件
* @param folderPath 文件夹路径例如 "folder1/subfolder/"
* @return 文件夹网络路径
*/
@PostMapping("/createFolder")
@ApiOperation("创建文件夹")
public AjaxResult createFolder(@RequestParam("folderName") String folderName)
public AjaxResult createFolder(@RequestParam("folderName") String folderPath)
{
sysFileService.createFolder(folderPath);
return AjaxResult.success("创建文件夹");
}
/**
* list 指定目录下所有文件信息文件名文件大小上传时间是否目录包括子文件夹
* @param folderName 文件夹名默认为根目录
* @param folderPath 要获取文件夹和文件的父文件夹路径例如 "folder1/"
* @return 文件夹网络路径
*/
@GetMapping("/listFiles")
public AjaxResult listFiles(@RequestParam("folderName") String folderName) {
return AjaxResult.success("获取指定目录下所有文件成功");
public AjaxResult listFiles(@RequestParam("folderName") String folderPath) {
return AjaxResult.success(sysFileService.getFilesAndSubfolders(folderPath));
}
/**
* 文件夹删除
* 从各个存储平台删除文件
* @param folderName 文件夹名默认为根目录
* @param folderPath 文件夹路径例如 "folder1/subfolder/"
* @return 文件夹网络路径
*/
@PostMapping("/deleteFolder")
public AjaxResult deleteFolder(@RequestParam("folderName") String folderName) {
public AjaxResult deleteFolder(@RequestParam("folderName") String folderPath) {
sysFileService.deleteFolder(folderPath);
return AjaxResult.success("文件夹删除成功");
}

View File

@ -0,0 +1,49 @@
package com.bonus.file.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
/**
* @author bonus
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class FileDetails {
/**
* 文件或文件夹的路径
*/
private String fileName;
/**
* 相对地址
*/
private String objectKey;
/**
* 文件大小文件夹默认为 0
*/
private long size;
/**
* 最后修改时间文件夹也有此属性
*/
private Date lastModified;
/**
* ETag文件夹一般没有 ETag
*/
private String etag;
/**
* 内容类型文件夹一般没有此信息
*/
private String contentType;
/**
* 是否是文件夹默认值为 false
*/
private boolean isFolder;
}

View File

@ -1,12 +1,12 @@
package com.bonus.file.service;
import com.bonus.common.core.domain.R;
import com.bonus.file.entity.FileDetails;
import com.bonus.system.api.domain.SysFile;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.FileOutputStream;
import java.util.List;
import java.util.Set;
/**
* 文件上传接口
@ -42,6 +42,16 @@ public interface ISysFileService
*/
public void downloadFile(HttpServletResponse response, String urlStr) throws Exception;
/**
* 从给定的URL下载文件并将其保存到指定的目标位置
*
* @param urlStr 要下载文件的URL地址
* @param response 请求响应
* @throws Exception 如果在下载过程中遇到任何异常例如网络问题文件写入问题等
*/
public void downloadFilesAsZip(HttpServletResponse response, List<String> urlStr) throws Exception;
/**
* 删除指定URL所指向的文件
*
@ -51,4 +61,37 @@ public interface ISysFileService
*/
public void deleteFile(String urlStr) throws Exception;
/**
* 删除指定URL所指向的文件
*
* @param urlStr 要删除文件的URL地址
* @return 布尔值指示文件删除是否成功如果文件被成功删除则返回true否则返回false
* @throws Exception 如果在删除文件的过程中遇到错误例如网络问题文件不存在或没有删除权限等
*/
public void deleteFiles(List<String> urlStr) throws Exception;
/**
* 获取指定文件夹下的所有文件夹和文件
*
* @param folderPath 要获取文件夹和文件的父文件夹路径例如 "folder1/"
* @return 返回一个包含所有文件夹和文件路径的集合
*/
public Set<FileDetails> getFilesAndSubfolders(String folderPath);
/**
* 创建文件夹通过上传带有路径的文件
*
* @param folderPath 文件夹路径例如 "folder1/subfolder/"
*/
public void createFolder(String folderPath);
/**
* 删除指定路径的所有对象从而删除文件夹
*
* @param folderPath 文件夹路径例如 "folder1/subfolder/"
*/
public void deleteFolder(String folderPath);
}

View File

@ -1,26 +1,29 @@
package com.bonus.file.service.impl;
import java.io.InputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import com.alibaba.nacos.common.utils.IoUtils;
import com.bonus.common.core.utils.file.FileTypeUtils;
import com.bonus.common.core.utils.file.MimeTypeUtils;
import com.bonus.file.entity.FileDetails;
import com.bonus.file.service.ISysFileService;
import com.bonus.file.utils.FileUploadUtils;
import com.bonus.system.api.domain.SysFile;
import com.github.tobato.fastdfs.domain.fdfs.StorePath;
import com.github.tobato.fastdfs.service.FastFileStorageClient;
import org.apache.commons.io.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import com.github.tobato.fastdfs.domain.fdfs.StorePath;
import com.github.tobato.fastdfs.service.FastFileStorageClient;
import com.bonus.common.core.utils.file.FileTypeUtils;
import org.apache.commons.io.IOUtils;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
/**
* FastDFS 文件存储
@ -101,6 +104,18 @@ public class FastDfsSysFileServiceImpl implements ISysFileService
}
}
/**
* 从给定的URL下载文件并将其保存到指定的目标位置
*
* @param response 请求响应
* @param urlStr 要下载文件的URL地址
* @throws Exception 如果在下载过程中遇到任何异常例如网络问题文件写入问题等
*/
@Override
public void downloadFilesAsZip(HttpServletResponse response, List<String> urlStr) throws Exception {
}
/**
* FastDfs文件删除接口待测试
*
@ -121,4 +136,47 @@ public class FastDfsSysFileServiceImpl implements ISysFileService
throw new Exception("Error occurred while deleting file from FastDfs" + urlStr, e);
}
}
/**
* 删除指定URL所指向的文件
*
* @param urlStr 要删除文件的URL地址
* @return 布尔值指示文件删除是否成功如果文件被成功删除则返回true否则返回false
* @throws Exception 如果在删除文件的过程中遇到错误例如网络问题文件不存在或没有删除权限等
*/
@Override
public void deleteFiles(List<String> urlStr) throws Exception {
}
/**
* 获取指定文件夹下的所有文件夹和文件
*
* @param folderPath 要获取文件夹和文件的父文件夹路径例如 "folder1/"
* @return 返回一个包含所有文件夹和文件路径的集合
*/
@Override
public Set<FileDetails> getFilesAndSubfolders(String folderPath) {
return Collections.emptySet();
}
/**
* 创建文件夹通过上传带有路径的文件
*
* @param folderPath 文件夹路径例如 "folder1/subfolder/"
*/
@Override
public void createFolder(String folderPath) {
}
/**
* 删除指定路径的所有对象从而删除文件夹
*
* @param folderPath 文件夹路径例如 "folder1/subfolder/"
*/
@Override
public void deleteFolder(String folderPath) {
}
}

View File

@ -1,29 +1,24 @@
package com.bonus.file.service.impl;
import com.bonus.common.core.utils.StringUtils;
import com.bonus.common.core.utils.file.FileUtils;
import com.bonus.file.entity.FileDetails;
import com.bonus.file.service.ISysFileService;
import com.bonus.file.utils.FileDownloadUtils;
import com.bonus.file.utils.FileUploadUtils;
import com.bonus.system.api.domain.SysFile;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Primary;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import com.bonus.file.utils.FileUploadUtils;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
/**
* 本地文件存储
@ -83,6 +78,18 @@ public class LocalSysFileServiceImpl implements ISysFileService
FileDownloadUtils.downloadFile(response, urlStr);
}
/**
* 从给定的URL下载文件并将其保存到指定的目标位置
*
* @param response 请求响应
* @param urlStr 要下载文件的URL地址
* @throws Exception 如果在下载过程中遇到任何异常例如网络问题文件写入问题等
*/
@Override
public void downloadFilesAsZip(HttpServletResponse response, List<String> urlStr) throws Exception {
}
@Override
public void deleteFile(String urlStr) throws Exception
{
@ -99,4 +106,49 @@ public class LocalSysFileServiceImpl implements ISysFileService
throw new Exception("删除文件时文件不存在");
}
}
/**
* 删除指定URL所指向的文件
*
* @param urlStr 要删除文件的URL地址
* @return 布尔值指示文件删除是否成功如果文件被成功删除则返回true否则返回false
* @throws Exception 如果在删除文件的过程中遇到错误例如网络问题文件不存在或没有删除权限等
*/
@Override
public void deleteFiles(List<String> urlStr) throws Exception {
for (String url : urlStr) {
deleteFile(url);
}
}
/**
* 获取指定文件夹下的所有文件夹和文件
*
* @param folderPath 要获取文件夹和文件的父文件夹路径例如 "folder1/"
* @return 返回一个包含所有文件夹和文件路径的集合
*/
@Override
public Set<FileDetails> getFilesAndSubfolders(String folderPath) {
return Collections.emptySet();
}
/**
* 创建文件夹通过上传带有路径的文件
*
* @param folderPath 文件夹路径例如 "folder1/subfolder/"
*/
@Override
public void createFolder(String folderPath) {
}
/**
* 删除指定路径的所有对象从而删除文件夹
*
* @param folderPath 文件夹路径例如 "folder1/subfolder/"
*/
@Override
public void deleteFolder(String folderPath) {
}
}

View File

@ -0,0 +1,222 @@
package com.bonus.file.service.impl;
import com.alibaba.nacos.common.utils.UuidUtils;
import com.bonus.common.core.utils.file.FileUtils;
import com.bonus.common.core.utils.file.MimeTypeUtils;
import com.bonus.file.entity.FileDetails;
import com.bonus.file.service.ISysFileService;
import com.bonus.file.utils.FileUploadUtils;
import com.bonus.file.utils.MinioUtil;
import com.bonus.system.api.domain.SysFile;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
/**
* Minio 文件存储
*/
@Service
@ConditionalOnProperty(name = "storage.type", havingValue = "minio")
public class MinioServiceImpl implements ISysFileService {
private static final Logger logger = LoggerFactory.getLogger(MinioServiceImpl.class);
@Resource
private MinioUtil minioUtil;
/**
* Minio 文件上传接口
*
* @param file 上传的文件
* @return SysFile对象或null
*/
@Override
public SysFile uploadFile(MultipartFile file) {
try {
FileUploadUtils.assertAllowed(file, MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION);
String originalFilename = Objects.requireNonNull(file.getOriginalFilename(), "文件名不能为空").replace(" ", "");
String extension = originalFilename.substring(originalFilename.lastIndexOf('.'));
String objectKey = FileUtils.generateObjectName(UuidUtils.generateUuid() + extension);
return minioUtil.uploadFile(file, objectKey);
} catch (Exception e) {
logger.error("文件上传失败: {}", file.getOriginalFilename(), e);
return null;
}
}
/**
* Minio 多文件上传接口
*
* @param files 上传的文件数组
* @return 上传成功的 SysFile 列表
*/
@Override
public List<SysFile> uploadFiles(MultipartFile[] files) throws Exception {
// 将每个文件的上传操作作为一个 CompletableFuture 任务
List<CompletableFuture<SysFile>> futures = new ArrayList<>();
for (MultipartFile file : files) {
CompletableFuture<SysFile> future = CompletableFuture.supplyAsync(() -> {
try {
return uploadFile(file);
} catch (Exception e) {
throw new RuntimeException("文件上传失败: " + file.getOriginalFilename(), e);
}
});
futures.add(future);
}
// 使用 allOf 等待所有任务完成并收集结果
CompletableFuture<Void> allOf = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
// 在所有上传任务完成后获取每个文件的上传结果
return allOf.thenApply(v -> futures.stream()
.map(CompletableFuture::join) // 获取每个 CompletableFuture 的结果
.collect(Collectors.toList())
).get(); // .get() 会阻塞直到所有任务完成
}
/**
* MinIO 下载文件
*
* @param response 响应对象
* @param urlStr 文件 URL 地址
*/
@Override
public void downloadFile(HttpServletResponse response, String urlStr) throws Exception {
try (InputStream inputStream = minioUtil.downloadFile(urlStr)) {
com.bonus.file.utils.FileUtils.setResponseHeaderByUrl(response, urlStr);
try (OutputStream outputStream = response.getOutputStream()) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
outputStream.flush();
}
} catch (IOException e) {
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
logger.error("下载文件失败: {}", urlStr, e);
throw new Exception("下载失败");
}
}
/**
* 从给定的URL下载文件并将其保存到指定的目标位置
*
* @param response 请求响应
* @param urlStr 要下载文件的URL地址
* @throws Exception 如果在下载过程中遇到任何异常例如网络问题文件写入问题等
*/
@Override
public void downloadFilesAsZip(HttpServletResponse response, List<String> urlStr) throws Exception {
String zipFileName = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")) + ".zip";
// 设置响应头
response.setContentType("application/zip");
response.setHeader("Content-Disposition", "attachment; filename=" + zipFileName);
try (ZipOutputStream zos = new ZipOutputStream(response.getOutputStream())) {
// 遍历文件列表并压缩
for (String objectName : urlStr) {
try (InputStream inputStream = minioUtil.downloadFile(objectName)) {
// 添加 ZipEntry
zos.putNextEntry(new ZipEntry(objectName));
byte[] buffer = new byte[1024];
int len;
while ((len = inputStream.read(buffer)) > 0) {
zos.write(buffer, 0, len);
}
zos.closeEntry();
} catch (Exception e) {
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
throw new Exception("文件压缩或写入失败: " + objectName, e);
}
}
zos.finish();
} catch (Exception e) {
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
throw new Exception("压缩文件下载失败", e);
}
}
/**
* 删除文件
*
* @param urlStr 文件 URL 地址
*/
@Override
public void deleteFile(String urlStr) throws Exception {
try {
minioUtil.deleteObject(urlStr);
logger.info("文件删除成功: {}", urlStr);
} catch (Exception e) {
logger.error("文件删除失败: {}", urlStr, e);
throw e;
}
}
/**
* 删除指定URL所指向的文件
*
* @param urlStr 要删除文件的URL地址
* @return 布尔值指示文件删除是否成功如果文件被成功删除则返回true否则返回false
* @throws Exception 如果在删除文件的过程中遇到错误例如网络问题文件不存在或没有删除权限等
*/
@Override
public void deleteFiles(List<String> urlStr){
for (String objectName : urlStr) {
try {
deleteFile(objectName);
} catch (Exception e) {
logger.error("文件删除失败: {} 错误: {}", objectName, e.getMessage());
}
}
}
/**
* 获取指定文件夹下的所有文件夹和文件
*
* @param folderPath 要获取文件夹和文件的父文件夹路径例如 "folder1/"
* @return 返回一个包含所有文件夹和文件路径的集合
*/
@Override
public Set<FileDetails> getFilesAndSubfolders(String folderPath) {
return minioUtil.getFilesAndSubfolders(folderPath);
}
/**
* 创建文件夹通过上传带有路径的文件
*
* @param folderPath 文件夹路径例如 "folder1/subfolder/"
*/
@Override
public void createFolder(String folderPath) {
minioUtil.createFolder(folderPath);
}
/**
* 删除指定路径的所有对象从而删除文件夹
*
* @param folderPath 文件夹路径例如 "folder1/subfolder/"
*/
@Override
public void deleteFolder(String folderPath) {
minioUtil.deleteFolder(folderPath);
}
}

View File

@ -1,13 +1,14 @@
package com.bonus.file.service.impl;
import com.bonus.common.core.utils.file.MimeTypeUtils;
import com.bonus.file.entity.FileDetails;
import com.bonus.file.service.ISysFileService;
import com.bonus.file.utils.FileUploadUtils;
import com.bonus.system.api.domain.SysFile;
import com.mongodb.client.gridfs.model.GridFSFile;
import lombok.RequiredArgsConstructor;
import org.apache.commons.io.IOUtils;
import org.bson.Document;
import org.bson.types.Binary;
import org.bson.types.ObjectId;
import org.springframework.beans.factory.annotation.Autowired;
@ -26,9 +27,12 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import org.bson.Document;
/**
* @author wangvivi
*/
@ -52,14 +56,11 @@ public class MongodbServiceImpl implements ISysFileService {
@Override
public SysFile uploadFile(MultipartFile file) throws Exception {
//验证文件扩展名和大小
FileUploadUtils.assertAllowed(file, MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION);
String fileName = Objects.requireNonNull(file.getOriginalFilename(), "文件名不能为空");
fileName = fileName.replace(" ", "");
long fileSize = file.getSize();
// 判断是否使用 GridFS 存储
if (fileSize >= gridFsSizeThreshold) {
// 使用 GridFS 存储大文件
@ -112,6 +113,45 @@ public class MongodbServiceImpl implements ISysFileService {
}
}
/**
* 从给定的URL下载文件并将其保存到指定的目标位置
*
* @param response 请求响应
* @param fileIds 要下载文件的URL地址
* @throws Exception 如果在下载过程中遇到任何异常例如网络问题文件写入问题等
*/
@Override
public void downloadFilesAsZip(HttpServletResponse response, List<String> fileIds) throws Exception {
String zipFileName = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")) + ".zip";
response.setContentType("application/zip");
response.setHeader("Content-Disposition", "attachment; filename=" + zipFileName);
try (ZipOutputStream zos = new ZipOutputStream(response.getOutputStream())) {
byte[] buffer = new byte[8192];
for (String fileId : fileIds) {
// 先尝试从 GridFS 中读取
GridFSFile gridFSFile = gridFsTemplate.findOne(new org.springframework.data.mongodb.core.query.Query()
.addCriteria(org.springframework.data.mongodb.core.query.Criteria.where("_id").is(new ObjectId(fileId))));
if (gridFSFile != null) {
try (InputStream inputStream = gridFsTemplate.getResource(gridFSFile).getInputStream()) {
// 创建一个新的 ZipEntry并使用文件名称
zos.putNextEntry(new ZipEntry(gridFSFile.getFilename()));
int len;
while ((len = inputStream.read(buffer)) > 0) {
zos.write(buffer, 0, len);
}
zos.closeEntry();
} catch (Exception e) {
System.err.println("无法压缩文件: " + fileId + " 错误: " + e.getMessage());
}
} else {
System.err.println("文件不存在,文件 ID: " + fileId);
}
}
zos.finish();
}
}
@Override
public void deleteFile(String fileId) throws Exception {
try {
@ -126,6 +166,51 @@ public class MongodbServiceImpl implements ISysFileService {
}
}
/**
* 删除指定URL所指向的文件
*
* @param fileId 要删除文件的URL地址
* @return 布尔值指示文件删除是否成功如果文件被成功删除则返回true否则返回false
* @throws Exception 如果在删除文件的过程中遇到错误例如网络问题文件不存在或没有删除权限等
*/
@Override
public void deleteFiles(List<String> fileId) throws Exception {
for (String url : fileId) {
}
}
/**
* 获取指定文件夹下的所有文件夹和文件
*
* @param folderPath 要获取文件夹和文件的父文件夹路径例如 "folder1/"
* @return 返回一个包含所有文件夹和文件路径的集合
*/
@Override
public Set<FileDetails> getFilesAndSubfolders(String folderPath) {
return Collections.emptySet();
}
/**
* 创建文件夹通过上传带有路径的文件
*
* @param folderPath 文件夹路径例如 "folder1/subfolder/"
*/
@Override
public void createFolder(String folderPath) {
}
/**
* 删除指定路径的所有对象从而删除文件夹
*
* @param folderPath 文件夹路径例如 "folder1/subfolder/"
*/
@Override
public void deleteFolder(String folderPath) {
}
private void downloadFromCollection(HttpServletResponse response, String fileId) throws IOException {
// 尝试从普通集合中获取二进制文件
Document fileDocument = mongoTemplate.findById(fileId, Document.class, COLLECTION_NAME);

View File

@ -1,20 +1,17 @@
package com.bonus.file.service.impl;
import com.alibaba.nacos.common.utils.UuidUtils;
import com.bonus.common.core.domain.R;
import com.bonus.common.core.utils.file.FileUtils;
import com.bonus.common.core.utils.file.MimeTypeUtils;
import com.bonus.file.entity.FileDetails;
import com.bonus.file.service.ISysFileService;
import com.bonus.file.utils.FileUploadUtils;
import com.bonus.file.utils.ObsUtils;
import com.alibaba.nacos.common.utils.UuidUtils;
import com.bonus.common.core.utils.file.FileUtils;
import com.bonus.common.core.web.domain.AjaxResult;
import com.bonus.system.api.domain.SysFile;
import com.obs.services.model.ObsObject;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
@ -23,12 +20,11 @@ import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
/**
* Minio 文件存储
@ -52,7 +48,6 @@ public class ObsServiceImpl implements ISysFileService {
try {
//验证文件扩展名和大小
FileUploadUtils.assertAllowed(file, MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION);
String originalFilename = Objects.requireNonNull(file.getOriginalFilename(), "文件名不能为空");
originalFilename = originalFilename.replace(" ", "");
String extension = originalFilename.substring(originalFilename.lastIndexOf('.'));
@ -83,7 +78,6 @@ public class ObsServiceImpl implements ISysFileService {
@Override
public void downloadFile(HttpServletResponse response, String urlStr) throws Exception {
R<ObsObject> obsObjectR = obsUtils.downloadFile(urlStr);
if (R.isError(obsObjectR)) {
response.setStatus(HttpStatus.BAD_REQUEST.value());
throw new Exception("文件不存在");
@ -95,7 +89,6 @@ public class ObsServiceImpl implements ISysFileService {
InputStream inputStream = obsObjectR.getData().getObjectContent();
com.bonus.file.utils.FileUtils.setResponseHeaderByUrl(response, urlStr);
try (OutputStream outputStream = response.getOutputStream()) {
byte[] buffer = new byte[8192];
int bytesRead;
@ -109,8 +102,90 @@ public class ObsServiceImpl implements ISysFileService {
}
}
/**
* 从给定的URL下载文件并将其保存到指定的目标位置
*
* @param response 请求响应
* @param urlStr 要下载文件的URL地址
* @throws Exception 如果在下载过程中遇到任何异常例如网络问题文件写入问题等
*/
@Override
public void downloadFilesAsZip(HttpServletResponse response, List<String> urlStr) throws Exception {
String zipFileName = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")) + ".zip";
response.setContentType("application/zip");
response.setHeader("Content-Disposition", "attachment; filename=" + zipFileName);
try (ZipOutputStream zos = new ZipOutputStream(response.getOutputStream())) {
byte[] buffer = new byte[8192];
for (String objectName : urlStr) {
R<ObsObject> obsObjectR = obsUtils.downloadFile(objectName);
if (R.isError(obsObjectR)) {
response.setStatus(HttpStatus.BAD_REQUEST.value());
throw new Exception("文件不存在");
}
if (obsObjectR.getData() == null) {
response.setStatus(HttpStatus.NOT_FOUND.value());
throw new Exception("文件不存在");
}
try (InputStream inputStream = obsObjectR.getData().getObjectContent()) {
zos.putNextEntry(new ZipEntry(objectName));
int len;
while ((len = inputStream.read(buffer)) > 0) {
zos.write(buffer, 0, len);
}
zos.closeEntry();
} catch (Exception e) {
System.err.println("无法压缩文件: " + objectName + " 错误: " + e.getMessage());
}
}
zos.finish();
}
}
@Override
public void deleteFile(String urlStr) throws Exception {
obsUtils.deleteFile(urlStr);
}
/**
* 删除指定URL所指向的文件
*
* @param urlStr 要删除文件的URL地址
* @return 布尔值指示文件删除是否成功如果文件被成功删除则返回true否则返回false
* @throws Exception 如果在删除文件的过程中遇到错误例如网络问题文件不存在或没有删除权限等
*/
@Override
public void deleteFiles(List<String> urlStr) throws Exception {
}
/**
* 获取指定文件夹下的所有文件夹和文件
*
* @param folderPath 要获取文件夹和文件的父文件夹路径例如 "folder1/"
* @return 返回一个包含所有文件夹和文件路径的集合
*/
@Override
public Set<FileDetails> getFilesAndSubfolders(String folderPath) {
return obsUtils.getFilesAndFolders(folderPath);
}
/**
* 创建文件夹通过上传带有路径的文件
*
* @param folderPath 文件夹路径例如 "folder1/subfolder/"
*/
@Override
public void createFolder(String folderPath) {
obsUtils.createFolder(folderPath);
}
/**
* 删除指定路径的所有对象从而删除文件夹
*
* @param folderPath 文件夹路径例如 "folder1/subfolder/"
*/
@Override
public void deleteFolder(String folderPath) {
obsUtils.deleteFolderAndContents(folderPath);
}
}

View File

@ -5,11 +5,11 @@ import com.aliyun.oss.model.OSSObject;
import com.bonus.common.core.domain.R;
import com.bonus.common.core.utils.file.FileUtils;
import com.bonus.common.core.utils.file.MimeTypeUtils;
import com.bonus.file.entity.FileDetails;
import com.bonus.file.service.ISysFileService;
import com.bonus.file.utils.FileUploadUtils;
import com.bonus.file.utils.OssUtils;
import com.bonus.system.api.domain.SysFile;
import org.apache.commons.lang3.ObjectUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@ -21,12 +21,14 @@ import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
/**
* @author jiang
@ -48,9 +50,6 @@ public class OssServiceImpl implements ISysFileService {
*/
@Override
public SysFile uploadFile(MultipartFile file) throws Exception {
if (ObjectUtils.isEmpty(file)) {
throw new Exception("文件名为空");
}
try {
//验证文件扩展名和大小
FileUploadUtils.assertAllowed(file, MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION);
@ -83,7 +82,6 @@ public class OssServiceImpl implements ISysFileService {
// 使用 allOf 等待所有任务完成并收集结果
CompletableFuture<Void> allOf = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
// 在所有上传任务完成后获取每个文件的上传结果
return allOf.thenApply(v -> futures.stream()
.map(CompletableFuture::join) // 获取每个 CompletableFuture 的结果
@ -117,9 +115,94 @@ public class OssServiceImpl implements ISysFileService {
}
}
/**
* 从给定的URL下载文件并将其保存到指定的目标位置
*
* @param response 请求响应
* @param urlStr 要下载文件的URL地址
* @throws Exception 如果在下载过程中遇到任何异常例如网络问题文件写入问题等
*/
@Override
public void downloadFilesAsZip(HttpServletResponse response, List<String> urlStr) throws Exception {
String zipFileName = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")) + ".zip";
response.setContentType("application/zip");
response.setHeader("Content-Disposition", "attachment; filename=" + zipFileName);
try (ZipOutputStream zos = new ZipOutputStream(response.getOutputStream())) {
byte[] buffer = new byte[8192];
for (String objectName : urlStr) {
R<OSSObject> ossObjectR = ossUtils.download(objectName);
if (ossObjectR.getData() == null) {
response.setStatus(HttpStatus.NOT_FOUND.value());
throw new Exception("未发现文件");
}
OSSObject ossObject = ossObjectR.getData();
try (InputStream inputStream = ossObject.getObjectContent()) {
zos.putNextEntry(new ZipEntry(objectName));
int len;
while ((len = inputStream.read(buffer)) > 0) {
zos.write(buffer, 0, len);
}
zos.closeEntry();
} catch (Exception e) {
System.err.println("无法压缩文件: " + objectName + " 错误: " + e.getMessage());
}
}
zos.finish();
}
}
@Override
public void deleteFile(String urlStr) throws Exception {
ossUtils.delete(urlStr);
}
/**
* 删除指定URL所指向的文件
*
* @param urlStr 要删除文件的URL地址
* @return 布尔值指示文件删除是否成功如果文件被成功删除则返回true否则返回false
* @throws Exception 如果在删除文件的过程中遇到错误例如网络问题文件不存在或没有删除权限等
*/
@Override
public void deleteFiles(List<String> urlStr) throws Exception {
for (String objectName : urlStr) {
try {
deleteFile(objectName);
} catch (Exception e) {
log.error("文件删除失败: {} 错误: {}", objectName, e.getMessage());
}
}
}
/**
* 获取指定文件夹下的所有文件夹和文件
*
* @param folderPath 要获取文件夹和文件的父文件夹路径例如 "folder1/"
* @return 返回一个包含所有文件夹和文件路径的集合
*/
@Override
public Set<FileDetails> getFilesAndSubfolders(String folderPath) {
return ossUtils.getFilesAndFolders(folderPath);
}
/**
* 创建文件夹通过上传带有路径的文件
*
* @param folderPath 文件夹路径例如 "folder1/subfolder/"
*/
@Override
public void createFolder(String folderPath) {
ossUtils.createFolder(folderPath);
}
/**
* 删除指定路径的所有对象从而删除文件夹
*
* @param folderPath 文件夹路径例如 "folder1/subfolder/"
*/
@Override
public void deleteFolder(String folderPath) {
ossUtils.deleteFolder(folderPath);
}
}

View File

@ -0,0 +1,478 @@
package com.bonus.file.utils;
import com.bonus.file.config.MinioConfig;
import com.bonus.file.entity.FileDetails;
import com.bonus.system.api.domain.SysFile;
import io.minio.*;
import io.minio.errors.MinioException;
import io.minio.http.Method;
import io.minio.messages.Item;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
/**
* MinioUtil 工具类
* 封装 MinIO 常用操作如文件上传下载删除复制等功能
* @author bonus
*/
@Component
public class MinioUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(MinioUtil.class);
@Resource
private MinioClient minioClient;
@Resource
private MinioConfig minioConfig;
/**
* 分片大小
*/
private static final long PART_SIZE = 5*1024*1024;
/**
* 初始化默认存储桶
* Spring 容器启动后自动调用检查默认存储桶是否存在若不存在则创建
*/
@PostConstruct
public void init() {
try {
if (bucketExists(minioConfig.getBucketName())) {
LOGGER.error("Bucket already exists: {}", minioConfig.getBucketName());
} else {
createBucket(minioConfig.getBucketName());
}
} catch (Exception e) {
LOGGER.error("Error initializing default bucket: {}",e.getMessage(),e);
throw new RuntimeException("Error initializing default bucket: " + e.getMessage(), e);
}
}
/**
* 检查指定存储桶是否存在
* @param bucketName 存储桶名称
* @return true 表示存在false 表示不存在
* @throws Exception 若检查过程中发生异常
*/
public boolean bucketExists(String bucketName) throws Exception {
return minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
}
/**
* 创建指定的存储桶
* @param bucketName 存储桶名称
* @throws Exception 若创建过程中发生异常
*/
public void createBucket(String bucketName) throws Exception {
if (bucketExists(bucketName)) {
LOGGER.error("Bucket already exists: {}", bucketName);
} else {
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
LOGGER.error("Bucket created successfully: {}", bucketName);
}
}
/**
*
* 上传文件到指定存储桶
* @param file MultipartFile 文件对象
* @return 上传后的文件访问 URL
* @throws Exception 若上传过程中发生异常
*/
public SysFile uploadFile(MultipartFile file, String folderPath) throws Exception {
if (file.getSize() < 10 * 1024 * 1024L) {
minioClient.putObject(PutObjectArgs.builder()
.bucket(minioConfig.getBucketName())
.object(folderPath)
// -1 表示不限制文件大小
.stream(file.getInputStream(), file.getInputStream().available(), -1)
.contentType(file.getContentType())
.build());
} else {
uploadLargeFile(folderPath, file);
}
return SysFile.builder()
.name(file.getOriginalFilename())
.url(folderPath).build();
}
/**
* 分片上传文件到指定文件夹
* @param file MultipartFile 文件对象
* @param folderPath 目标文件夹路径 "folder/subfolder"
* @throws Exception 若上传过程中发生异常
*/
public void uploadLargeFile(String folderPath,MultipartFile file) throws Exception {
long fileSize = file.getSize();
int partCount = (int) Math.ceil((double) fileSize / PART_SIZE);
List<String> partNames = new ArrayList<>();
// 创建线程池
ExecutorService executor = Executors.newFixedThreadPool(5);
List<CompletableFuture<Void>> futures = new ArrayList<>();
// 上传每个分片
for (int i = 0; i < partCount; i++) {
long offset = i * PART_SIZE;
long currentPartSize = Math.min(PART_SIZE, fileSize - offset);
// 设置分片名称
String partObjectName = folderPath + "part." + i;
partNames.add(partObjectName);
final long partOffset = offset;
final long partSizeFinal = currentPartSize;
final String partName = partObjectName;
// 异步上传每个分片
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
try (InputStream inputStream = file.getInputStream()) { // 获取文件的输入流
// 跳过文件前面的部分直到当前分片的起始位置
long skipped = inputStream.skip(partOffset);
if (skipped != partOffset) {
throw new RuntimeException("Could not skip to the correct part offset.");
}
byte[] buffer = new byte[(int) partSizeFinal]; // 创建缓冲区来存储分片数据
int bytesRead = inputStream.read(buffer); // 读取分片数据
if (bytesRead == -1) {
throw new RuntimeException("Error reading the part data.");
}
// 上传分片
try (ByteArrayInputStream stream = new ByteArrayInputStream(buffer)) {
minioClient.putObject(PutObjectArgs.builder()
.bucket(minioConfig.getBucketName())
.object(partName)
.stream(stream, stream.available(), -1)
.build());
System.out.println("Uploaded part: " + partName);
}
} catch (Exception e) {
throw new RuntimeException("Error uploading part: " + partName, e);
}
}, executor); // 指定使用线程池执行任务
futures.add(future); // 将任务添加到 futures 列表
}
// 等待所有分片上传完成
CompletableFuture<Void> allOf = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
allOf.join(); // 阻塞等待所有任务完成
// 合并所有分片
List<ComposeSource> sources = new ArrayList<>();
for (String partName : partNames) {
sources.add(ComposeSource.builder().bucket(minioConfig.getBucketName()).object(partName).build());
}
// 将所有分片合并成最终文件
try {
minioClient.composeObject(ComposeObjectArgs.builder()
.bucket(minioConfig.getBucketName())
// 最终文件的路径
.object(folderPath) // 可以自定义最终文件名
.sources(sources)
.build());
System.out.println("Large file uploaded and composed successfully.");
} catch (MinioException e) {
throw new Exception("Error during file composition: " + e.getMessage(), e);
}
// 删除临时分片
for (String partName : partNames) {
minioClient.removeObject(RemoveObjectArgs.builder().bucket(minioConfig.getBucketName()).object(partName).build());
System.out.println("Removed part: " + partName);
}
System.out.println("Large file uploaded successfully to folder: " + folderPath);
}
/**
* 下载指定文件 InputStream 形式返回
* @param objectName 存储对象名称文件名
* @return 文件的输入流
*/
public InputStream downloadFile(String objectName){
try (InputStream inputStream = minioClient.getObject(GetObjectArgs.builder()
.bucket(minioConfig.getBucketName())
.object(objectName)
.build())) {
return inputStream;
} catch (Exception e) {
LOGGER.error("Error downloading file: {}", e.getMessage(), e);
return null;
}
}
/**
* 将多个文件压缩为 ZIP 并上传至 MinIO返回压缩文件的下载链接
* @param bucketName 存储桶名称
* @param objectNames 文件对象列表文件名
* @param zipFileName 压缩后的文件名
* @return 压缩文件的下载 URL
* @throws Exception
*/
public String downloadFilesAsZip(String bucketName, List<String> objectNames, String zipFileName) throws Exception {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
ZipOutputStream zos = new ZipOutputStream(baos)) {
// 压缩每个文件
for (String objectName : objectNames) {
try (InputStream inputStream = minioClient.getObject(GetObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.build())) {
// 添加 ZipEntry
zos.putNextEntry(new ZipEntry(objectName));
byte[] buffer = new byte[1024];
int len;
while ((len = inputStream.read(buffer)) > 0) {
zos.write(buffer, 0, len);
}
zos.closeEntry();
}
}
zos.finish();
// 上传 ZIP 文件到 MinIO
try (ByteArrayInputStream zipInputStream = new ByteArrayInputStream(baos.toByteArray())) {
minioClient.putObject(PutObjectArgs.builder()
.bucket(bucketName)
.object(zipFileName)
.stream(zipInputStream, zipInputStream.available(), -1)
.contentType("application/zip")
.build());
}
}
// 返回压缩文件的下载 URL
return getFileUrl(bucketName, zipFileName);
}
/**
* 获取文件的临时访问 URL指定过期时间
* @param bucketName 存储桶名称
* @param objectName 存储对象名称文件名
* @param expiryTimeInSeconds URL 的有效时长
* @return 文件的临时访问 URL
* @throws Exception 若生成 URL 过程中发生异常
*/
public String getFileUrl(String bucketName, String objectName, int expiryTimeInSeconds) throws Exception {
return minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
.bucket(bucketName)
.object(objectName)
.expiry(expiryTimeInSeconds)
.method(Method.GET)
.build());
}
/**
* 获取文件的临时访问 URL默认过期时间为 7
* @param bucketName 存储桶名称
* @param objectName 存储对象名称文件名
* @return 文件的临时访问 URL
* @throws Exception 若生成 URL 过程中发生异常
*/
public String getFileUrl(String bucketName, String objectName) throws Exception {
// 604800 = 7
return getFileUrl(bucketName, objectName, 604800);
}
/**
* 删除指定的对象
* @param objectName 存储对象名称文件名
* @throws Exception 若删除过程中发生异常
*/
public void deleteObject( String objectName) throws Exception {
minioClient.removeObject(RemoveObjectArgs.builder()
.bucket(minioConfig.getBucketName())
.object(objectName)
.build());
}
/**
* 获取指定文件夹下的所有文件夹和文件
*
* @param folderPath 要获取文件夹和文件的父文件夹路径例如 "folder1/"
* @return 返回一个包含所有文件夹和文件详细信息的集合
*/
public Set<FileDetails> getFilesAndSubfolders(String folderPath) {
if (!folderPath.endsWith("/")) {
folderPath += "/";
}
// 存储文件和子文件夹详细信息的集合
Set<FileDetails> filesAndSubfolders = new HashSet<>();
try {
// 列出指定文件夹路径下的所有对象文件夹和文件
Iterable<Result<Item>> objects = minioClient.listObjects(
ListObjectsArgs.builder()
.bucket(minioConfig.getBucketName())
.prefix(folderPath) // 设置父文件夹路径前缀
.delimiter("/") // 使用分隔符来模拟获取子文件夹
.build()
);
// 遍历所有对象提取文件夹和文件路径
for (Result<Item> result : objects) {
Item item = result.get();
String objectName = item.objectName();
// 如果是文件夹路径包含 / 且没有文件名部分则添加子文件夹
if (objectName.startsWith(folderPath)) {
String subfolder = objectName.substring(folderPath.length());
if (subfolder.contains("/")) {
// 如果是子文件夹提取文件夹名称并添加至集合
filesAndSubfolders.add(new FileDetails(
subfolder.substring(0, subfolder.indexOf("/")),
null,
0,
new Date(),
null,
null,
true
));
}
}
}
// 获取文件详细信息
Iterable<Result<Item>> objectsWithoutDelimiter = minioClient.listObjects(
ListObjectsArgs.builder()
.bucket(minioConfig.getBucketName())
.prefix(folderPath)
.build()
);
// 遍历文件对象获取文件详细信息
for (Result<Item> result : objectsWithoutDelimiter) {
Item item = result.get();
String objectName = item.objectName();
// 过滤掉文件夹路径以 '/' 结尾
if (!objectName.endsWith("/")) {
// 获取文件的详细信息
StatObjectResponse stat = minioClient.statObject(
StatObjectArgs.builder()
.bucket(minioConfig.getBucketName())
.object(objectName)
.build()
);
// 封装文件详细信息到 FileDetails 实体类
FileDetails fileDetails = new FileDetails(
objectName.substring(objectName.lastIndexOf("/") + 1),
objectName,
stat.size(),
Date.from(stat.lastModified().toInstant()),
stat.etag(),
stat.contentType(),
false
);
filesAndSubfolders.add(fileDetails);
}
}
// 输出文件和子文件夹路径
System.out.println("Files and subfolders under " + folderPath + ": " + filesAndSubfolders);
} catch (Exception e) {
e.printStackTrace();
}
return filesAndSubfolders;
}
/**
* 创建文件夹通过上传带有路径的文件
*
* @param folderPath 文件夹路径例如 "folder1/subfolder/"
*/
public void createFolder(String folderPath) {
try {
if (!folderPath.endsWith("/")) {
folderPath += "/";
}
// 创建一个空文件内容模拟文件夹
// 空文件内容
String emptyFileContent = "";
InputStream emptyFileStream = new ByteArrayInputStream(emptyFileContent.getBytes());
// 上传一个空文件到指定路径从而模拟文件夹创建
minioClient.putObject(
PutObjectArgs.builder()
.bucket(minioConfig.getBucketName())
// 使用文件夹路径 + 文件名
.object(folderPath + "empty.txt")
.stream(emptyFileStream, 0, -1)
.contentType("application/text")
.build()
);
} catch (Exception e) {
// 使用 LOGGER 打印异常日志
LOGGER.error("Error occurred while fetching files from bucket: {}",minioConfig.getBucketName(), e);
}
}
/**
* 删除指定路径的所有对象从而删除整个文件夹及其子文件夹
*
* @param folderPath 文件夹路径例如 "folder1/subfolder/"
*/
public void deleteFolder(String folderPath) {
try {
if (!folderPath.endsWith("/")) {
folderPath += "/";
}
// 列出指定文件夹路径下的所有对象包括子文件夹中的对象
Iterable<Result<Item>> objects = minioClient.listObjects(
ListObjectsArgs.builder()
.bucket(minioConfig.getBucketName())
.prefix(folderPath) // 设置路径前缀列出该路径下的所有对象
.recursive(true) // 递归列出所有子文件夹中的对象
.build()
);
// 遍历所有对象并删除它们
for (Result<Item> result : objects) {
Item item = result.get();
String objectName = item.objectName();
// 删除对象
minioClient.removeObject(
RemoveObjectArgs.builder()
.bucket(minioConfig.getBucketName())
.object(objectName)
.build()
);
System.out.println("Deleted object: " + objectName);
}
System.out.println("Folder and all subfolders deleted successfully at path: " + folderPath);
} catch (Exception e) {
System.err.println("Error deleting folder and its contents at path: " + folderPath);
e.printStackTrace();
}
}
}

View File

@ -2,17 +2,19 @@ package com.bonus.file.utils;
import com.bonus.common.core.domain.R;
import com.bonus.file.config.ObsConfig;
import com.bonus.file.entity.FileDetails;
import com.bonus.system.api.domain.SysFile;
import com.obs.services.ObsClient;
import com.obs.services.exception.ObsException;
import com.obs.services.model.*;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
@ -170,4 +172,127 @@ public class ObsUtils {
}
}
/**
* 删除指定路径的所有对象从而删除整个文件夹及其子文件夹
*
* @param folderPath 文件夹路径例如 "folder1/subfolder/"
*/
public void deleteFolderAndContents(String folderPath) {
try {
ListObjectsRequest request = new ListObjectsRequest(obsConfig.getBucket());
request.setPrefix(folderPath); // 设置前缀为文件夹路径
request.setDelimiter("/"); // 使用分隔符来获取文件夹内容
ObjectListing objectListing;
do {
// 列出对象
objectListing = obsClient.listObjects(request);
List<ObsObject> objects = objectListing.getObjects();
// 遍历对象并删除它们
for (ObsObject object : objects) {
String objectKey = object.getObjectKey();
DeleteObjectResult result = obsClient.deleteObject(obsConfig.getBucket(), objectKey);
if (result.isDeleteMarker()) {
System.out.println("Deleted object: " + objectKey);
}
}
// 获取下一页的 marker并在 request 中设置继续下一次分页
request.setMarker(objectListing.getNextMarker());
} while (objectListing.isTruncated()); // isTruncated true 表示有下一页
System.out.println("Folder and all subfolders deleted successfully at path: " + folderPath);
} catch (ObsException e) {
System.err.println("Error deleting folder and its contents at path: " + folderPath);
e.printStackTrace();
}
}
/**
* OBS 中创建文件夹
*
* @param folderPath 文件夹路径例如 "folder1/subfolder/"
*/
public void createFolder(String folderPath) {
try {
// 确保 folderPath "/" 结尾这样 OBS 会将其视为文件夹
if (!folderPath.endsWith("/")) {
folderPath += "/";
}
// 创建一个空的字节流作为文件夹占位文件的内容
ByteArrayInputStream emptyContent = new ByteArrayInputStream(new byte[0]);
// 构建上传请求
PutObjectRequest request = new PutObjectRequest(obsConfig.getBucket(), folderPath, emptyContent);
// 上传空文件创建文件夹
obsClient.putObject(request);
System.out.println("Folder created successfully at path: " + folderPath);
} catch (ObsException e) {
System.err.println("Error creating folder at path: " + folderPath);
e.printStackTrace();
}
}
/**
* 获取指定文件夹下所有文件夹和文件的详细信息
*
* @param folderPath 要获取文件夹和文件的父文件夹路径例如 "folder1/"
* @return 返回包含所有文件夹和文件详细信息的集合
*/
public Set<FileDetails> getFilesAndFolders(String folderPath) {
Set<FileDetails> filesAndFolders = new HashSet<>();
try {
ListObjectsRequest request = new ListObjectsRequest(obsConfig.getBucket());
request.setPrefix(folderPath);
request.setDelimiter("/");
ObjectListing objectListing;
do {
objectListing = obsClient.listObjects(request);
// 处理文件夹
for (String commonPrefix : objectListing.getCommonPrefixes()) {
filesAndFolders.add(new FileDetails(
commonPrefix,
null,
0,
null,
null,
null,
true // isFolder
));
}
// 处理文件
for (ObsObject object : objectListing.getObjects()) {
filesAndFolders.add(new FileDetails(
object.getObjectKey().substring(object.getObjectKey().lastIndexOf("/") + 1),
object.getObjectKey(),
object.getMetadata().getContentLength(),
Date.from(object.getMetadata().getLastModified().toInstant()),
object.getMetadata().getEtag(),
object.getMetadata().getContentType(),
false // isFolder
));
}
request.setMarker(objectListing.getNextMarker());
} while (objectListing.isTruncated());
} catch (ObsException e) {
System.err.println("Error listing files and folders at path: " + folderPath);
e.printStackTrace();
}
return filesAndFolders;
}
}

View File

@ -4,11 +4,10 @@ import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.model.*;
import com.bonus.common.core.domain.R;
import com.bonus.common.core.text.Convert;
import com.bonus.common.core.utils.file.FileUtils;
import com.bonus.file.config.OSSConfig;
import com.bonus.file.entity.FileDetails;
import com.bonus.system.api.domain.SysFile;
import org.apache.poi.ss.formula.functions.T;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@ -16,13 +15,14 @@ import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import com.bonus.system.api.domain.SysFile;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
@ -218,4 +218,119 @@ public class OssUtils {
throw new Exception(e.getMessage());
}
}
/**
* 获取指定文件夹下所有文件夹和文件的详细信息
*
* @param folderPath 要获取文件夹和文件的父文件夹路径例如 "folder1/"
* @return 返回包含所有文件夹和文件详细信息的集合
*/
public Set<FileDetails> getFilesAndFolders(String folderPath) {
Set<FileDetails> filesAndFolders = new HashSet<>();
try {
ListObjectsRequest request = new ListObjectsRequest(ossConfig.getBucket());
request.setPrefix(folderPath);
request.setDelimiter("/");
ObjectListing objectListing;
do {
objectListing = ossClient.listObjects(request);
// 处理文件夹
for (String commonPrefix : objectListing.getCommonPrefixes()) {
filesAndFolders.add(new FileDetails(
commonPrefix,
null,
0,
null,
null,
null,
true // isFolder
));
}
// 处理文件
for (OSSObjectSummary objectSummary : objectListing.getObjectSummaries()) {
filesAndFolders.add(new FileDetails(
objectSummary.getKey().substring(objectSummary.getKey().lastIndexOf("/") + 1),
objectSummary.getKey(),
objectSummary.getSize(),
objectSummary.getLastModified(),
objectSummary.getETag(),
objectSummary.getType(),
false // isFolder
));
}
request.setMarker(objectListing.getNextMarker());
} while (objectListing.isTruncated());
} catch (Exception e) {
System.err.println("Error listing files and folders at path: " + folderPath);
e.printStackTrace();
}
return filesAndFolders;
}
/**
* OSS 中创建文件夹
*
* @param folderPath 文件夹路径例如 "folder1/subfolder/"
*/
public void createFolder(String folderPath) {
try {
// 设置文件夹的元数据
ObjectMetadata metadata = new ObjectMetadata();
// 文件内容长度设为 0表示空文件
metadata.setContentLength(0);
// 通过空文件创建文件夹
ossClient.putObject(ossConfig.getBucket(), folderPath, new ByteArrayInputStream(new byte[0]), metadata);
System.out.println("Folder created successfully at path: " + folderPath);
} catch (Exception e) {
System.err.println("Error creating folder at path: " + folderPath);
e.printStackTrace();
}
}
/**
* 删除指定路径的所有对象从而删除整个文件夹及其子文件夹
*
* @param folderPath 文件夹路径例如 "folder1/subfolder/"
*/
public void deleteFolder( String folderPath) {
try {
String nextMarker = null;
ObjectListing objectListing;
do {
// 列出指定路径下的所有对象
objectListing = ossClient.listObjects(ossConfig.getBucket(), folderPath);
List<String> keysToDelete = new ArrayList<>();
// 获取对象的键并添加到删除列表
for (OSSObjectSummary summary : objectListing.getObjectSummaries()) {
keysToDelete.add(summary.getKey());
}
// 删除所有列出的对象
if (!keysToDelete.isEmpty()) {
DeleteObjectsRequest deleteRequest = new DeleteObjectsRequest(ossConfig.getBucket()).withKeys(keysToDelete);
ossClient.deleteObjects(deleteRequest);
System.out.println("Deleted objects: " + keysToDelete);
}
// 获取下一页标记
nextMarker = objectListing.getNextMarker();
} while (objectListing.isTruncated()); // 如果有更多对象则继续
System.out.println("Folder and all subfolders deleted successfully at path: " + folderPath);
} catch (Exception e) {
System.err.println("Error deleting folder at path: " + folderPath);
e.printStackTrace();
}
}
}