拉取镜像
docker pull registry.cn-hangzhou.aliyuncs.com/qiluo-images/minio:latest
运行命令
docker run -d \
--name minio \
-p 10087:9000 \
-p 10088:9001 \
-e MINIO_ROOT_USER=minioadmin \
-e MINIO_ROOT_PASSWORD=Y6HYraaphfZ9k8Lv \
-v /data/minio/data:/data \
-v /data/minio/config:/root/.minio \
-e TZ=Asia/Shanghai \
--log-driver json-file \
--log-opt max-size=10m \
--log-opt max-file=3 \
registry.cn-hangzhou.aliyuncs.com/qiluo-images/minio:latest \
server /data --console-address ":9001" --address ":9000"
日志驱动:使用 json-file 驱动,并限制日志大小和文件数量(避免日志无限增长)。
然后ip+10088 访问, 然后创建桶和密钥
Docker Compose 脚本
docker-compose.yml
version: '3.8'
services:
minio:
image: registry.cn-hangzhou.aliyuncs.com/qiluo-images/minio:latest
container_name: minio
ports:
- "10087:9000"
- "10088:9001"
environment:
- MINIO_ROOT_USER=minioadmin
- MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD} # 使用环境变量或 .env 文件来保护密码
- TZ=Asia/Shanghai
volumes:
- /data/minio/data:/data
- /data/minio/config:/root/.minio
networks:
- minio-network
restart: always # 容器崩溃后自动重启
logging:
driver: json-file
options:
max-size: "10m" # 限制日志文件大小
max-file: "3" # 保留的日志文件数量
networks:
minio-network:
driver: bridge
MINIO_ROOT_USER 和 MINIO_ROOT_PASSWORD 使用了环境变量,建议将密码放入 .env 文件中,避免直接暴露在 docker-compose.yml 中。
vi .env
MINIO_ROOT_PASSWORD=Y6HYraaphfZ9k8Lv
持久化数据和配置:
持久化存储的数据目录和配置文件目录分别挂载到 /data/minio/data 和 /data/minio/config,确保容器重启后数据和配置不会丢失。
自定义网络:
使用了一个名为 minio-network 的自定义桥接网络。这样可以为 Minio 容器提供一个隔离的网络环境,方便容器间通信。
日志管理:
使用 json-file 日志驱动,设置最大日志文件大小为 10m,并限制保留最多 3 个日志文件,防止日志文件占用过多磁盘空间。
自动重启:
设置 restart: always,确保容器崩溃或机器重启时自动启动。
运行以下命令启动 Minio 服务:
docker-compose up -d
如果要停止服务,可以运行:
docker-compose down
然后pom中引入
<aws.version>2.29.12</aws.version>
<!-- 文件 -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
<version>${aws.version}</version>
</dependency>
基于pg数据库的实体类
package com.cqcloud.platform.entity;
import java.io.Serial;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 系统基础信息--文件管理表
* @author weimeilayer@gmail.com
* @date 2021-12-13 16:28:32
*/
@Data
@TableName("public.sys_file")
@EqualsAndHashCode(callSuper = false)
@JsonIgnoreProperties(ignoreUnknown = true)
@Schema(description = "系统基础信息--文件管理表")
public class SysFile extends Model<SysFile> {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(value = "id", type = IdType.ASSIGN_ID)
@Schema(description = "主键ID")
private String id;
/**
* 原文件名
*/
@Schema(description = "原文件名")
private String name;
/**
* 存储桶名称
*/
@Schema(description = "原始文件名")
private String original;
/**
* 分组编号,用于对应多文件
*/
@Schema(description = "分组编号,用于对应多文件")
private String groupId;
/**
* 文件类型
*/
@Schema(description = "文件类型")
private String fileType;
/**
* 文件后缀
*/
@Schema(description = "文件后缀")
private String suffix;
/**
* 文件大小,单位字节
*/
@Schema(description = "文件大小,单位字节")
private Integer size;
/**
* 预览地址
*/
@Schema(description = "预览地址")
private String previewUrl;
/**
* 存储类型
*/
@Schema(description = "存储类型")
private String storageType;
/**
* 存储地址
*/
@Schema(description = "存储地址")
private String storageUrl;
/**
* 桶名
*/
@Schema(description = "桶名")
private String bucketName;
/**
* 桶内文件名
*/
@Schema(description = "桶内文件名")
private String objectName;
/**
* 访问次数
*/
@Schema(description = "访问次数")
private Integer visitCount;
/**
* 排序
*/
@Schema(description = "排序")
private Integer sort;
/**
* 备注
*/
@Schema(description = "备注")
private String remarks;
/**
* 逻辑删除(0:未删除;null:已删除)
*/
@Schema(description = "逻辑删除(0:未删除;null:已删除)")
private String delFlag;
/**
* 创建人
*/
@Schema(description = "创建人")
@TableField(fill = FieldFill.INSERT)
private String createBy;
/**
* 编辑人
*/
@Schema(description = "编辑人")
@TableField(fill = FieldFill.UPDATE)
private String updateBy;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
@Schema(description = "创建时间")
private LocalDateTime gmtCreate;
/**
* 编辑时间
*/
@Schema(description = "编辑时间")
@TableField(fill = FieldFill.UPDATE)
private LocalDateTime gmtModified;
/**
* 所属租户
*/
@Schema(description = "所属租户")
private String tenantId;
}
package com.cqcloud.platform.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import software.amazon.awssdk.regions.Region;
/**
* aws 配置信息bucket 设置公共读权限
* @author weimeilayer@gmail.com
* @date 💓💕2023年4月1日🐬🐇 💓💕
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "minio")
public class MinioProperties {
/**
* 对象存储服务的URL
*/
@Schema(description = "对象存储服务的URL")
private String endpoint;
/**
* 自定义域名
*/
@Schema(description = "自定义域名")
private String customDomain;
/**
* 反向代理和S3默认支持
*/
@Schema(description = "反向代理和S3默认支持")
private Boolean pathStyleAccess = true;
/**
* 应用ID
*/
@Schema(description = "应用ID")
private String appId;
/**
* 区域
*/
@Schema(description = "区域")
private String region= Region.CN_NORTH_1.toString();
/**
* 预览地址
*/
@Schema(description = "预览地址")
private String previewDomain;
/**
* Access key就像用户ID,可以唯一标识你的账户
*/
@Schema(description = "Access key就像用户ID,可以唯一标识你的账户")
private String accessKey;
/**
* Secret key是你账户的密码
*/
@Schema(description = "Secret key是你账户的密码")
private String secretKey;
/**
* 默认的存储桶名称
*/
@Schema(description = "默认的存储桶名称")
private String bucketName;
/**
* 公开桶名
*/
@Schema(description = "公开桶名")
private String publicBucketName;
/**
* 物理删除文件
*/
@Schema(description = "物理删除文件")
private boolean physicsDelete;
/**
* 最大线程数,默认: 100
*/
@Schema(description = "最大线程数,默认: 100")
private Integer maxConnections = 100;
}
package com.cqcloud.platform.config;
import java.io.InputStream;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.annotation.Configuration;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.core.ResponseInputStream;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.S3ClientBuilder;
import software.amazon.awssdk.services.s3.S3Configuration;
import software.amazon.awssdk.services.s3.model.Bucket;
import software.amazon.awssdk.services.s3.model.CreateBucketRequest;
import software.amazon.awssdk.services.s3.model.DeleteBucketRequest;
import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
import software.amazon.awssdk.services.s3.model.ListObjectsV2Request;
import software.amazon.awssdk.services.s3.model.ListObjectsV2Response;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import software.amazon.awssdk.services.s3.model.PutObjectResponse;
import software.amazon.awssdk.services.s3.model.S3Object;
/**
* aws-s3 通用存储操作 支持所有兼容s3协议的云存储: {阿里云OSS,腾讯云COS,七牛云,京东云,minio 等}
*
* @author weimeilayer@gmail.com ✨
* @date 💓💕2024年3月7日🐬🐇 💓💕
*/
@Configuration
@RequiredArgsConstructor
public class MinioTemplate implements InitializingBean {
private final MinioProperties ossProperties;
private S3Client s3Client;
/**
* 创建bucket
*
* @param bucketName bucket名称
*/
@SneakyThrows
public void createBucket(String bucketName) {
if (!s3Client.listBuckets().buckets().stream().anyMatch(b -> b.name().equals(bucketName))) {
s3Client.createBucket(CreateBucketRequest.builder().bucket(bucketName).build());
}
}
/**
* 获取全部bucket
*/
@SneakyThrows
public List<Bucket> getAllBuckets() {
return s3Client.listBuckets().buckets();
}
/**
* @param bucketName bucket名称
*/
@SneakyThrows
public Optional<Bucket> getBucket(String bucketName) {
return s3Client.listBuckets().buckets().stream().filter(b -> b.name().equals(bucketName)).findFirst();
}
/**
* 删除bucket
*
* @param bucketName bucket名称
*/
@SneakyThrows
public void removeBucket(String bucketName) {
s3Client.deleteBucket(DeleteBucketRequest.builder().bucket(bucketName).build());
}
/**
* 根据文件前缀查询文件
*
* @param bucketName bucket名称
* @param prefix 前缀
* @param recursive 是否递归查询
* @return S3ObjectSummary 列表
*/
public List<S3Object> getAllObjectsByPrefix(String bucketName, String prefix, boolean recursive) {
// 构建查询请求
ListObjectsV2Request request = ListObjectsV2Request.builder().bucket(bucketName).prefix(prefix).build();
// 获取查询结果
ListObjectsV2Response response = s3Client.listObjectsV2(request);
// 返回文件列表
return new ArrayList<>(response.contents());
}
/**
* 获取文件
*
* @param bucketName bucket名称
* @param objectName 文件名称
* @return 二进制流
*/
public ResponseInputStream<GetObjectResponse> getObject(String bucketName, String objectName) {
// 构建获取对象请求
GetObjectRequest request = GetObjectRequest.builder().bucket(bucketName).key(objectName).build();
// 获取对象并返回
return s3Client.getObject(request);
}
/**
* 上传文件
*
* @param bucketName bucket名称
* @param objectName 文件名称
* @param stream 文件流
*/
public void putObject(String bucketName, String objectName, InputStream stream) throws Exception {
putObject(bucketName, objectName, stream, (long) stream.available(), "application/octet-stream");
}
/**
* 上传文件
*
* @param bucketName bucket名称
* @param objectName 文件名称
* @param stream 文件流
* @param size 大小
* @param contextType 类型
*/
public PutObjectResponse putObject(String bucketName, String objectName, InputStream stream, long size,
String contextType) throws Exception {
byte[] bytes = new byte[(int) size];
stream.read(bytes);
RequestBody requestBody = RequestBody.fromBytes(bytes);
PutObjectRequest request = PutObjectRequest.builder().bucket(bucketName).key(objectName).contentLength(size)
.contentType(contextType).build();
return s3Client.putObject(request, requestBody);
}
/**
* 添加一个公开方法来获取 S3Client
*
* @return
*/
public S3Client getS3Client() {
return this.s3Client;
}
/**
* 删除文件
*
* @param bucketName bucket名称
* @param objectName 文件名称
*/
public void removeObject(String bucketName, String objectName) throws Exception {
s3Client.deleteObject(DeleteObjectRequest.builder().bucket(bucketName).key(objectName).build());
}
@Override
public void afterPropertiesSet() throws Exception {
S3ClientBuilder s3ClientBuilder = S3Client.builder().endpointOverride(URI.create(ossProperties.getEndpoint()))
.region(Region.of(ossProperties.getRegion()))
.credentialsProvider(
() -> AwsBasicCredentials.create(ossProperties.getAccessKey(), ossProperties.getSecretKey()))
.serviceConfiguration(
S3Configuration.builder().pathStyleAccessEnabled(ossProperties.getPathStyleAccess()).build());
this.s3Client = s3ClientBuilder.build();
}
}
在application-dev.yml
配置
# 文件系统
minio:
endpoint: 上传文件服务器的ip+ 10087
access-key: `您的ak`
secret-key: `您的sk`
bucket-name: `桶名`
public-bucket-name: `公共桶名`
preview-domain: 最后访问或者互联网地址的,域名这种的地址,为了安全`需要权限访问`这个就可以不配置,如果是公共访问,那么就只需要在实现层注释的那个取消即可
package com.cqcloud.platform.service;
import java.io.IOException;
import org.springframework.web.multipart.MultipartFile;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.cqcloud.platform.common.matter.utils.Result;
import com.cqcloud.platform.dto.SysFileDto;
import com.cqcloud.platform.entity.SysFile;
import com.cqcloud.platform.vo.SysFileSelVo;
import com.google.zxing.WriterException;
import jakarta.servlet.http.HttpServletResponse;
/**
* 系统基础信息--文件管理服务类
* @author weimeilayer@gmail.com ✨
* @date 💓💕 2023年5月20日 🐬🐇 💓💕
*/
public interface SysFileService extends IService<SysFile> {
/**
* 分页查询SysFile
* @param selvo 查询参数
* @return
*/
public IPage<SysFileDto> getSysFileDtoPage(Page page,SysFileSelVo selvo);
/**
* 上传文件
* @param file
* @return
*/
public Result uploadFile(MultipartFile file,String groupId,Integer sort);
/**
* 读取文件
* @param fileId
* @param response
*/
public void getFile(String fileId, HttpServletResponse response);
/**
* 删除文件
* @param id
* @return
*/
public Boolean deleteFile(String id);
/**
* 根据id预览文件
* @param fileId
* @param response
*/
public void previewFile(String fileId, HttpServletResponse response);
/**
* 根据idname预览文件
* @param fileName
* @param response
*/
public void previewByFileName(String fileName, HttpServletResponse response);
}
package com.cqcloud.platform.service.impl;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.cqcloud.platform.common.matter.utils.MultipartFileUtil;
import com.cqcloud.platform.common.matter.utils.QRCodeUtil;
import com.cqcloud.platform.common.matter.utils.Result;
import com.cqcloud.platform.config.MinioProperties;
import com.cqcloud.platform.config.MinioTemplate;
import com.cqcloud.platform.dto.SysFileDto;
import com.cqcloud.platform.entity.SysFile;
import com.cqcloud.platform.handler.FileTypeMagicHandler;
import com.cqcloud.platform.mapper.SysFileMapper;
import com.cqcloud.platform.service.SysFileMagicConfigService;
import com.cqcloud.platform.service.SysFileMagicErrorRecordService;
import com.cqcloud.platform.service.SysFileService;
import com.cqcloud.platform.vo.SysFileSelVo;
import com.google.zxing.WriterException;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
/**
* 系统基础信息--文件管理服务实现类
*
* @author weimeilayer@gmail.com ✨
* @date 💓💕 2023年5月20日 🐬🐇 💓💕
*/
@Slf4j
@Service
@AllArgsConstructor
public class SysFileServiceImpl extends ServiceImpl<SysFileMapper, SysFile> implements SysFileService {
private final MinioTemplate minioTemplate;
private final MinioProperties minioProperties;
/**
* 上传文件
*
* @param file
* @return
*/
@Override
public Result uploadFile(MultipartFile file,String groupId,Integer sort) {
String fileId = UUID.randomUUID().toString().replace("-", "");
// 如果 groupId 为空,使用默认值 "defaultGroupId"
groupId = (groupId == null) ? "defaultGroupId" : groupId;
// 如果 sort 为空,使用默认值 0
sort = (sort == null) ? 0 : sort;
String originalFilename = new String(Objects.requireNonNull(file.getOriginalFilename()).getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
// 生成文件名
String fileName = IdUtil.simpleUUID() + StrUtil.DOT + FileUtil.extName(originalFilename);
// 生成文件目录,格式为 yyyy/MM/dd/
String dir = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd/"));
// 拼接完整路径,存储在 Minio 中的完整路径
String fullFilePath = dir + fileName;
// 准备返回结果
Map<String, String> resultMap = new LinkedHashMap<>(6);
resultMap.put("bucketName", minioProperties.getBucketName());
resultMap.put("fileName", fileName);
resultMap.put("originalFilename", originalFilename);
resultMap.put("fileId", fileId);
//String domain = minioProperties.getPreviewDomain();
resultMap.put("previewById", String.format("/api/sysfile/preview/%s", fileId));
resultMap.put("url", String.format("/api/sysfile/previewByFileName/%s", fileName));
//resultMap.put("url", String.format("%s/api/sysfile/previewByFileName/%s",domain, fileName));
try (InputStream inputStream = file.getInputStream()) {
// 上传文件到 Minio
minioTemplate.putObject(minioProperties.getBucketName(), fullFilePath, inputStream, file.getSize(), file.getContentType());
// 文件管理数据记录
minioInsertToDb(file,groupId, fileName, fileId,fullFilePath,sort);
} catch (Exception e) {
log.error("上传失败", e);
return Result.failed(e.getLocalizedMessage());
}
return Result.ok(resultMap);
}
/**
* 删除文件
*
* @param id
* @return
*/
@Override
@SneakyThrows
@Transactional(rollbackFor = Exception.class)
public Boolean deleteFile(String id) {
SysFile file = this.getById(id);
minioTemplate.removeObject(minioProperties.getBucketName(), file.getName());
return file.updateById();
}
/**
* 文件管理数据记录,收集管理追踪文件
*
* @param file 上传文件格式
* @param fileName 文件名
*/
private void minioInsertToDb(MultipartFile file,String groupId,String fileName,String fileId,String fullFilePath,Integer sort) {
String originalFilename = new String(Objects.requireNonNull(file.getOriginalFilename()).getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
String suffix = FileUtil.extName(file.getOriginalFilename());
SysFile sysFile = new SysFile();
sysFile.setId(fileId);
sysFile.setName(fileName);
sysFile.setPreviewUrl(fullFilePath);
sysFile.setOriginal(originalFilename);
sysFile.setSize((int) file.getSize());
sysFile.setFileType(suffix);
sysFile.setBucketName(minioProperties.getBucketName());
sysFile.setDelFlag(StringPool.ZERO);
sysFile.setGroupId(groupId);
sysFile.setSuffix(suffix);
sysFile.setStorageType("minio");
sysFile.setObjectName(fileName);
sysFile.setVisitCount(0);
sysFile.setSort(sort);
this.save(sysFile);
}
/**
* 分页查询SysFile
*
* @param page
* @param selvo 查询参数
* @return
*/
@Override
public IPage<SysFileDto> getSysFileDtoPage(Page page, SysFileSelVo selvo) {
return baseMapper.getSysFileDtoPage(page, selvo);
}
@Override
public void getFile(String fileId, HttpServletResponse response) {
// 1. 根据文件ID查找文件的元数据
SysFile sf = this.getById(fileId);
if (sf == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
// 获取文件所在的桶(Bucket)和文件路径
String bucketName = sf.getBucketName();
String fullFilePath = sf.getPreviewUrl();
try {
// 2. 从Minio获取文件输入流
InputStream fileInputStream = minioTemplate.getObject(bucketName, fullFilePath);
// 获取文件的Content-Type
String contentType = URLConnection.guessContentTypeFromName(fileId);
if (contentType == null) {
contentType = "application/octet-stream";
}
// 设置响应头(可选,根据需要设置)
response.setContentType(contentType);
String encodedFileName = URLEncoder.encode(fileId, StandardCharsets.UTF_8.toString()).replaceAll("\\+", "%20");
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + encodedFileName + "\"");
response.setContentLengthLong(sf.getSize());
// 3. 写文件内容到响应输出流
try (OutputStream outputStream = response.getOutputStream()) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = fileInputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
outputStream.flush();
}
} catch (Exception e) {
log.error("读取文件失败", e);
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
try {
response.getWriter().write("读取文件失败: " + e.getMessage());
} catch (IOException ioException) {
log.error("Error writing error message to response", ioException);
}
}
}
@Override
public void previewFile(String fileId, HttpServletResponse response) {
// 1. 根据文件ID查找文件的元数据
SysFile sf = this.getById(fileId);
if (sf == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
// 获取文件所在的桶(Bucket)和文件路径
String bucketName = sf.getBucketName();
String fullFilePath = sf.getPreviewUrl();
try {
// 2. 从Minio获取文件输入流
InputStream fileInputStream = minioTemplate.getObject(bucketName, fullFilePath);
// 获取文件的Content-Type
String contentType = URLConnection.guessContentTypeFromName(fullFilePath);
if (contentType == null) {
contentType = "application/octet-stream";
}
// 设置响应头
response.setContentType(contentType); // 预设文件类型
String encodedFileName = URLEncoder.encode(fileId, StandardCharsets.UTF_8.toString()).replaceAll("\\+", "%20");
// 设置 Content-Disposition 为 inline,这样浏览器会尝试预览文件
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + encodedFileName + "\"");
response.setContentLengthLong(sf.getSize()); // 设置文件大小
// 3. 写文件内容到响应输出流
try (OutputStream outputStream = response.getOutputStream()) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = fileInputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
outputStream.flush();
}
} catch (Exception e) {
log.error("读取文件失败,文件名: {}, 错误信息: {}", fileId, e.getMessage(), e);
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
try {
response.getWriter().write("读取文件失败,请稍后重试!");
} catch (IOException ioException) {
log.error("响应错误消息失败", ioException);
}
}
}
@Override
public void previewByFileName(String fileName, HttpServletResponse response) {
// 1. 根据文件ID查找文件的元数据
SysFile sf = this.getOne(Wrappers.<SysFile>query().checkSqlInjection().lambda().eq(SysFile::getObjectName, fileName));
if (sf == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
// 获取文件所在的桶(Bucket)和文件路径
String bucketName = sf.getBucketName();
String fullFilePath = sf.getPreviewUrl();
try {
// 2. 从Minio获取文件输入流
InputStream fileInputStream = minioTemplate.getObject(bucketName, fullFilePath);
// 获取文件的Content-Type
String contentType = URLConnection.guessContentTypeFromName(fullFilePath);
if (contentType == null) {
contentType = "application/octet-stream";
}
// 设置响应头
response.setContentType(contentType); // 预设文件类型
String encodedFileName = URLEncoder.encode(sf.getId(), StandardCharsets.UTF_8).replaceAll("\\+", "%20");
// 设置 Content-Disposition 为 inline,这样浏览器会尝试预览文件
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + encodedFileName + "\"");
response.setContentLengthLong(sf.getSize()); // 设置文件大小
// 3. 写文件内容到响应输出流
try (OutputStream outputStream = response.getOutputStream()) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = fileInputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
outputStream.flush();
}
} catch (Exception e) {
log.error("读取文件失败,文件名: {}, 错误信息: {}", fileName, e.getMessage(), e);
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
try {
response.getWriter().write("读取文件失败,请稍后重试!");
} catch (IOException ioException) {
log.error("响应错误消息失败", ioException);
}
}
}
}
package com.cqcloud.platform.controller;
import java.io.IOException;
import com.cqcloud.platform.common.security.annotation.SasIgnore;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.http.HttpHeaders;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.cqcloud.platform.common.log.annotation.SysLog;
import com.cqcloud.platform.common.matter.utils.Result;
import com.cqcloud.platform.dto.SysFileDto;
import com.cqcloud.platform.service.SysFileService;
import com.cqcloud.platform.vo.SysFileSelVo;
import com.google.zxing.WriterException;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
/**
* 系统基础信息--文件管理模块
* @author weimeilayer@gmail.com
* @date 2021-12-13 16:28:32
*/
@RestController
@AllArgsConstructor
@RequestMapping("/sysfile")
@Tag(description = "系统基础信息--文件管理模块操作接口", name = "系统基础信息--文件管理模块操作接口")
@SecurityRequirement(name = HttpHeaders.AUTHORIZATION)
public class SysFileController {
private final SysFileService sysFileService;
/**
* 分页查询文件信息列表
* @param page
* @return
*/
@GetMapping("/pagelist")
@SysLog("分页查询文件信息")
@Operation(summary="分页查询文件信息", description = "分页查询文件信息")
public Result<IPage<SysFileDto>> getSysFileDtoPage(@ParameterObject Page page,@ParameterObject SysFileSelVo selvo) {
return Result.ok(sysFileService.getSysFileDtoPage(page, selvo));
}
/**
* 上传文件 文件名采用uuid
* @param file 资源
* @return
*/
@SysLog("上传文件")
@PostMapping("upload")
@Parameters({ @Parameter(name = "groupId", description = "分组编号,用于对应多文件", example = "1"),
@Parameter(name = "sort", description = "排序", required = true, example = "1") })
@Operation(summary = "上传文件", description = "上传文件")
public Result upload(@RequestParam("file") MultipartFile file,String groupId,Integer sort) {
if (file.isEmpty()) {
return Result.failed("文件上传失败");
}
return sysFileService.uploadFile(file,groupId,sort);
}
/**
* 获取文件
* @param fileId
* @param response
*/
@SysLog("获取文件")
@Operation(summary = "获取文件", description = "获取文件")
@GetMapping("/{fileId}")
public void file(@PathVariable String fileId, HttpServletResponse response) {
sysFileService.getFile(fileId, response);
}
/**
* 根据id预览文件
*
* @param fileId 文件ID
* @param response
* @return
*/
@Operation(summary = "根据id预览文件", description = "根据文件ID预览文件")
@GetMapping("/preview/{fileId}")
public void previewFile(@PathVariable String fileId, HttpServletResponse response) {
sysFileService.previewFile(fileId, response);
}
/**
* 根据文件名字预览文件
* @param fileName 文件ID
* @param response
* @return
*/
@SasIgnore(value = false)
@Operation(summary = "根据文件名字预览文件", description = "根据文件名字预览文件")
@GetMapping("/previewByFileName/{fileName}")
public void previewByFileName(@PathVariable("fileName") String fileName, HttpServletResponse response) {
sysFileService.previewByFileName(fileName, response);
}
}