最新的springboot 3.x的支持s3协议的2.x方法的minio上传文件方法

news2025/2/24 10:59:53

拉取镜像

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);
	}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2256523.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

cocos creator接入字节跳动抖音小游戏JSAPI敏感词检测(进行文字输入,但输入敏感词后没有替换为*号)

今天更新了某个抖音小游戏的版本&#xff0c;增加了部分剧情&#xff0c;半天过后一条短信审核未通过&#xff0c;emmm…抖音总是能给开发者惊喜…打开电脑看看这次又整什么幺蛾子… 首先是一脸懵逼&#xff0c;后端早已接入了官方的内容安全检测能力了&#xff08;https://de…

Origin快速拟合荧光寿命、PL Decay (TRPL)数据分析处理-方法二

1.先导入数据到origin 2.导入文件的时候注意&#xff1a;名字短的这个是&#xff0c;或者你打开后看哪个里面有800&#xff0c;因为我的激光重频是1.25Hz&#xff08;应该是&#xff0c;不太确定单位是KHz还是MHz&#xff09;&#xff0c;所以对应的时间是800s。 3.选中两列直接…

17. 面向对象的特征

一、面向对象的三大特征 面向对象的三大特征指的是 封装、继承、多态。 封装&#xff08;encapsulation&#xff0c;有时称为数据隐藏&#xff09;是处理对象的一个重要概念。从形式上看&#xff0c;封装就是将数据和行为组合在一个包中&#xff0c;并对对象的使用者隐藏具体的…

Apache Dolphinscheduler可视化 DAG 工作流任务调度系统

Apache Dolphinscheduler 关于 一个分布式易扩展的可视化 DAG 工作流任务调度系统。致力于解决数据处理流程中错综复杂的依赖关系&#xff0c;使调度系统在数据处理流程中开箱即用。 DolphinScheduler 的主要特性如下&#xff1a; 易于部署&#xff0c;提供四种部署方式&am…

第二部分:基础知识 6.函数 --[JavaScript 新手村:开启编程之旅的第一步]

JavaScript 函数是可重用的代码块&#xff0c;用于执行特定任务。函数可以接受参数&#xff08;输入数据&#xff09;&#xff0c;并且可以返回一个值。JavaScript 提供了多种定义函数的方式&#xff0c;下面将详细介绍这些方式&#xff0c;并给出一些示例。 1. 函数声明 下面…

我眼中的“懂重构”(一)

初识重构 2017年的时候&#xff0c;领导让我看公司的一本书《重构——改善代码的既有设计》&#xff0c;这是一本JAVA版本的&#xff0c;前后看了2遍。那时候看书因为不懂看的格外仔细。我只是那时候不懂&#xff0c;然而多年后的今天我仍然发现很多人对重构充满误解。在刚进入…

机器学习详解(3):线性回归之代码详解

文章目录 1 数据预处理2 构建线性回归模型并绘制回归线初始化方法前向传播&#xff1a;forward_propagation代价函数&#xff1a;cost_function反向传播&#xff1a;backward_propagation参数更新&#xff1a;update_parameters训练方法&#xff1a;train代码运行结果 3 使用Py…

基于openzeppelin插件的智能合约升级

一、作用以及优点 部署可升级合约&#xff0c;插件自动部署proxy和proxyAdmin合约&#xff0c;帮助管理合约升级和交互&#xff1b;升级已部署合约&#xff0c;通过插件快速升级合约&#xff0c;脚本开发方便快捷&#xff1b;管理代理管理员的权限&#xff0c;只有proxyAdmin的…

游戏引擎学习第36天

仓库 :https://gitee.com/mrxiao_com/2d_game 回顾之前的内容 在这个程序中&#xff0c;目标是通过手动编写代码来从头开始制作一个完整的游戏。整个过程不使用任何库或现成的游戏引擎&#xff0c;这样做的目的是为了能够全面了解游戏执行的每一个细节。开发过程中&#xff0…

试题转excel;pdf转excel;试卷转Excel,word试题转excel

一、问题描述 一名教师朋友&#xff0c;偶尔会需要整理一些高质量的题目到excel中 以往都是手动复制搬运&#xff0c;几百道题几乎需要一个下午的时间 关键这些事&#xff0c;枯燥无聊费眼睛&#xff0c;实在是看起来就很蠢的工作 就想着做一个工具&#xff0c;可以自动处理…

16-01、JVM系列之:内存与垃圾回收篇(一)

JVM系列之&#xff1a;内存与垃圾回收篇&#xff08;一&#xff09; ##本篇内容概述&#xff1a; 1、JVM结构 2、类加载子系统 3、运行时数据区之&#xff1a;PC寄存器、Java栈、本地方法栈一、JVM与JAVA体系结构 JAVA虚拟机与JAVA语言并没有必然的联系&#xff0c;它只是与特…

2030. gitLab A仓同步到B仓

文章目录 1 A 仓库备份 到 B 仓库2 B 仓库修改main分支的权限 1 A 仓库备份 到 B 仓库 #!/bin/bash# 定义变量 REPO_DIR"/home/xhome/opt/git_sync/zz_xx_xx" # 替换为你的本地库A的实际路径 REMOTE_ORIGIN"http://192.168.1.66:8181/zzkj_software/zz_xx_xx.…

服务器上部署前端页面-实现IP+端口/index.html在线访问你的网页

首先一点&#xff0c;不管是那个框架开发的网页前端&#xff0c;最后都需要Build,构建完毕以后都是原始的HTML CSS JS 三样文件&#xff01; 所以不管用原始的三剑客&#xff08;HTML CSS JS&#xff09;开发的前端还是用各类框架开发的前端界面&#xff08;只是让开发简单…

树莓派 PICO RP2040 MACOS 使用

文章参考&#xff1a; Developing in C on the RP2040: macOS | Wellys Dev 这里会提示报错&#xff1a;ln: /bin/picotool: Operation not permitted 参考&#xff1a;Mac ln命令报错&#xff1a;Operation not permitted_ln operation not permitted-CSDN博客 放在 usr/lo…

顶顶通电话机器人开发接口对接大语言模型之实时流TTS对接介绍

大语言模型一般都是流式返回文字&#xff0c;如果等全部文字返回了一次性去TTS&#xff0c;那么延迟会非常严重&#xff0c;常用的方法就是通过标点符号断句&#xff0c;返回了一句话就提交给TTS。随着流TTS的出现&#xff0c;就可以直接把大模型返回的文字灌给流TTS&#xff0…

git使用-创建本地仓库、绑定远程仓库

文章目录 1. 创建git仓库2. commit提交到本地3. 创建远程仓库4. 关联远程仓库5. push代码至远程仓库6. 完成初始化 git作为版本控制工具&#xff0c;在开发过程中经常使用到。这里以github为例&#xff0c;简单介绍下仓库的创建及绑定&#xff0c;方便忘记了能快速的想起来。 1…

JavaScript 中通过Array.sort() 实现多字段排序、排序稳定性、随机排序洗牌算法、优化排序性能,JS中排序算法的使用详解(附实际应用代码)

目录 JavaScript 中通过Array.sort() 实现多字段排序、排序稳定性、随机排序洗牌算法、优化排序性能&#xff0c;JS中排序算法的使用详解&#xff08;附实际应用代码&#xff09; 一、为什么要使用Array.sort() 二、Array.sort() 的使用与技巧 1、基础语法 2、返回值 3、…

为什么Unity里的变体数和UWA工具测出来的不一样

1&#xff09;为什么Unity里的变体数和UWA工具测出来的不一样 2&#xff09;使用TextureArray为什么会导致L1 Cache Miss率变高 3&#xff09;Gfx.PresentFrame耗时异常高 4&#xff09;AO方案中哪个更适合移动端 这是第412篇UWA技术知识分享的推送&#xff0c;精选了UWA社区的…

汽车免拆案例 | 2007款宝马650i车发动机偶尔无法起动

故障现象 一辆2007款宝马650i车&#xff0c;搭载N62B48B发动机&#xff0c;累计行驶里程约为26万km。车主反映&#xff0c;发动机偶尔无法起动&#xff0c;故障频率较低&#xff0c;十几天出现1 次&#xff0c;且故障出现时起动机不工作。 故障诊断  接车后试车&#xff0c;…

DataEase 是开源的 BI 工具

DataEase 是开源的 BI 工具&#xff0c;帮助用户快速分析数据并洞察业务趋势&#xff0c;从而实现业务的改进与优化。DataEase 支持丰富的数据源连接&#xff0c;能够通过拖拉拽方式快速制作图表&#xff0c; DataEase 的优势&#xff1a; 开源开放&#xff1a;零门槛&#xf…