minio文件存储+ckplayer视频播放(minio分片上传合并视频播放)

news2024/11/13 22:59:46

文章目录

  • 参考
  • 简述
  • 效果
  • 启动minio
  • 代码
    • 配置类
      • RedisConfig
      • WebConfig
      • MinioClientAutoConfiguration
      • OSSProperties
      • application.yml
    • 实体类
      • MinioObject
      • Result
      • StatusCode
      • OssFile
      • OssPolicy
    • 工具类
      • FileTypeUtil
      • Md5Util
      • MediaType
      • MinioTemplate
    • 文件分片上传与合并
      • MinioFileController
      • MinioService
        • MinioServiceImpl
      • upload.html
    • 视频播放
      • VideoController
      • video.html
    • 测试
      • 上传
      • 播放1
      • 播放2

参考

来源:MInIO入门-04 基于minio+ckplayer视频点播 实现,minio-demo-video - Gitee代码地址

视频分片上传Minio和播放

简述

文件在前端经过分片,将分片上传到后台服务器,后台服务器传到minio。所有分片上传完成后,前端根据bucketName和objectName从后台服务器获取资源,而后台读取请求的range范围响应流给前端播放。

(优化点:1. 文件分片上传合并操作直接让前端和minio之间交互,而后台只生成每个分片的上传凭证 2. 视频播放不需要经过后台,而是由后台生成该objectName对应的签名url给前端,前端直接找minio获取流)

效果

在这里插入图片描述

启动minio

minio.exe server D:\software\work_software\minio\data --console-address :18001 --address :18000 > D:\software\work_software\minio\minio.log

在这里插入图片描述

代码

配置类

RedisConfig

@Configuration
public class RedisConfig {
    @Value("${spring.redis.host}")
    private String redisHost;

    @Value("${spring.redis.port}")
    private String redisPort;

    /**
     * 通过配置RedisStandaloneConfiguration实例来
     * 创建Redis Standolone模式的客户端连接创建工厂
     * 配置hostname和port
     *
     * @return LettuceConnectionFactory
     */
    @Bean
    public JedisConnectionFactory redisConnectionFactory() {
        return new JedisConnectionFactory(new RedisStandaloneConfiguration(redisHost, Integer.parseInt(redisPort)));
    }

    /**
     * 保证序列化之后不会乱码的配置
     *
     * @param connectionFactory connectionFactory
     * @return RedisTemplate
     */
    @Bean(name = "jsonRedisTemplate")
    public RedisTemplate<String, Serializable> redisTemplate(JedisConnectionFactory connectionFactory) {
        return getRedisTemplate(connectionFactory, genericJackson2JsonRedisSerializer());
    }

    /**
     * 解决:
     * org.springframework.data.redis.serializer.SerializationException:
     * Could not write JSON: Java 8 date/time type `java.time.LocalDateTime` not supported
     *
     * @return GenericJackson2JsonRedisSerializer
     */
    @Bean
    @Primary // 当存在多个Bean时,此bean优先级最高
    public GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer() {
        ObjectMapper objectMapper = new ObjectMapper();
        // 解决查询缓存转换异常的问题
        objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
                ObjectMapper.DefaultTyping.NON_FINAL,
                JsonTypeInfo.As.WRAPPER_ARRAY);
        // 支持 jdk 1.8 日期   ---- start ---
        objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        objectMapper.registerModule(new Jdk8Module())
                .registerModule(new JavaTimeModule())
                .registerModule(new ParameterNamesModule());
        // --end --
        return new GenericJackson2JsonRedisSerializer(objectMapper);
    }

    /**
     * 注入redis分布式锁实现方案redisson
     *
     * @return RedissonClient
     */
    @Bean
    public RedissonClient redisson() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://" + redisHost + ":" + redisPort).setDatabase(0);
        return Redisson.create(config);
    }

    /**
     * 采用jdk序列化的方式
     *
     * @param connectionFactory connectionFactory
     * @return RedisTemplate
     */
    @Bean(name = "jdkRedisTemplate")
    public RedisTemplate<String, Serializable> redisTemplateByJdkSerialization(JedisConnectionFactory connectionFactory) {
        return getRedisTemplate(connectionFactory, new JdkSerializationRedisSerializer());
    }

    private RedisTemplate<String, Serializable> getRedisTemplate(JedisConnectionFactory connectionFactory,
                                                                 RedisSerializer<?> redisSerializer) {
        RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate<>();
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(redisSerializer);

        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(redisSerializer);
        connectionFactory.afterPropertiesSet();
        redisTemplate.setConnectionFactory(connectionFactory);
        return redisTemplate;
    }
}

WebConfig

@Configuration
public class WebConfig implements WebMvcConfigurer {

    private static final String[] CLASSPATH_RESOURCE_LOCATIONS = {
            "classpath:/META-INF/resources/", "classpath:/resources/",
            "classpath:/static/", "classpath:/public/"};


    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/**")
                .addResourceLocations(CLASSPATH_RESOURCE_LOCATIONS);
    }
}

MinioClientAutoConfiguration

@Slf4j
@Configuration
@EnableConfigurationProperties(OSSProperties.class)
public class MinioClientAutoConfiguration {
    /**
     * 初始化MinioTemplate,封装了一些MinIOClient的基本操作
     *
     * @return MinioTemplate
     */
    @ConditionalOnMissingBean(MinioTemplate.class)
    @Bean(name = "minioTemplate")
    public MinioTemplate minioTemplate() {
        return new MinioTemplate();
    }
}

OSSProperties

@ConfigurationProperties(value = "oss.minio")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class OSSProperties {
    /**
     * 对象存储服务的URL
     */
    private String endpoint;

    /**
     * Access key就像用户ID,可以唯一标识你的账户。
     */
    private String accessKey;

    /**
     * Secret key是你账户的密码。
     */
    private String secretKey;

    /**
     * bucketName是你设置的桶的名称
     */
    private String bucketName;
}

application.yml

server:
  port: 18002
spring:
  application:
    name: minio-application
  servlet:
    multipart:
      max-file-size: 100MB
      max-request-size: 100MB
  redis:
    database: 0
    host: 127.0.0.1
    port: 6379
    jedis:
      pool:
        max-active: 200
        max-wait: -1
        max-idle: 10
        min-idle: 0
    timeout: 2000
  thymeleaf:
    #模板的模式,支持 HTML, XML TEXT JAVASCRIPT
    mode: HTML5
    #编码 可不用配置
    encoding: UTF-8
    #开发配置为false,避免修改模板还要重启服务器
    cache: false
    #配置模板路径,默认是templates,可以不用配置
    prefix: classpath:/templates/
    suffix: .html
    servlet:
      content-type: text/html
oss:
  minio:
    endpoint: http://127.0.0.1:18000
    accessKey: qwiVxtzgeYbGSEZuV9ki
    secretKey: UeM1Rj6kkrpB5LSHf4xSPOBXwu34CmUmEt9sAcnm
    bucketName: minio-demo



实体类

MinioObject

@Data
@AllArgsConstructor
@NoArgsConstructor
public class MinioObject {
    private String bucket;
    private String region;
    private String object;
    private String etag;
    private long size;
    private boolean deleteMarker;
    private Map<String, String> userMetadata;
}

Result

@Slf4j
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Result<T> {
    private String message;
    private Integer code;
    private T data;


    /**
     * 成功 并不返回数据
     * @param <T>
     * @return
     */
    public static <T> Result<T> ok() {
        return new Result<>(StatusCode.SUCCESS.getMessage(), StatusCode.SUCCESS.getCode(), null);
    }

    /**
     * 成功 并返回数据
     * @param data
     * @param <T>
     * @return
     */
    public static <T> Result<T> ok(T data) {
        return new Result<>(StatusCode.SUCCESS.getMessage(), StatusCode.SUCCESS.getCode(), data);
    }

    /**
     * 系统错误 不返回数据
     * @param <T>
     * @return
     */
    public static <T> Result<T> error() {
        return new Result<>(StatusCode.FAILURE.getMessage(), StatusCode.FAILURE.getCode(), null);
    }

    /**
     * 系统错误 并返回逻辑数据
     * @param data
     * @param <T>
     * @return
     */
    public static <T> Result<T> error(T data) {
        return new Result<>(StatusCode.FAILURE.getMessage(), StatusCode.FAILURE.getCode(), data);
    }

    /**
     * 错误并返回指定错误信息和状态码以及逻辑数据
     * @param statusCode
     * @param data
     * @param <T>
     * @return
     */
    public static <T> Result<T> error(StatusCode statusCode, T data) {
        return new Result<>(statusCode.getMessage(), statusCode.getCode(), data);
    }

    /**
     * 错误并返回指定错误信息和状态码 不返回数据
     * @param statusCode
     * @param <T>
     * @return
     */
    public static <T> Result<T> error(StatusCode statusCode) {
        return new Result<>(statusCode.getMessage(), statusCode.getCode(), null);
    }

    /**
     * 自定义错误和状态返回
     * @param message
     * @param code
     * @param data
     * @param <T>
     * @return
     */
    public static <T> Result<T> errorMessage(String message, Integer code, T data) {
        return new Result<>(message, code, data);
    }

    /**
     * 自定义错误信息 状态码固定
     * @param message
     * @param <T>
     * @return
     */
    public static <T> Result<T> errorMessage(String message) {
        return new Result<>(message, StatusCode.CUSTOM_FAILURE.getCode(), null);
    }
}

StatusCode

public enum StatusCode {
    SUCCESS(20000, "操作成功"),
    PARAM_ERROR(40000, "参数异常"),
    NOT_FOUND(40004, "资源不存在"),
    FAILURE(50000, "系统异常"),
    CUSTOM_FAILURE(50001, "自定义异常错误"),
    ALONE_CHUNK_UPLOAD_SUCCESS(20001, "分片上传成功的标识"),
    ALL_CHUNK_UPLOAD_SUCCESS(20002, "所有的分片均上传成功");

    @Getter
    private final Integer code;
    @Getter
    private final String message;

    StatusCode(Integer code, String message) {
        this.code = code;
        this.message = message;
    }
}

OssFile

@Data
@NoArgsConstructor
@AllArgsConstructor
public class OssFile {
    /**
     * OSS 存储时文件路径
     */
    private String ossFilePath;
    /**
     * 原始文件名
     */
    private String originalFileName;
}

OssPolicy

/**
 * | 参数      | 说明                                                         |
 * | --------- | ------------------------------------------------------------ |
 * | Version   | 标识策略的版本号,Minio中一般为"**2012-10-17**"              |
 * | Statement | 策略授权语句,描述策略的详细信息,包含Effect(效果)、Action(动作)、Principal(用户)、Resource(资源)和Condition(条件)。其中Condition为可选 |
 * | Effect    | Effect(效果)作用包含两种:Allow(允许)和Deny(拒绝),系统预置策略仅包含允许的授权语句,自定义策略中可以同时包含允许和拒绝的授权语句,当策略中既有允许又有拒绝的授权语句时,遵循Deny优先的原则。 |
 * | Action    | Action(动作)对资源的具体操作权限,格式为:服务名:资源类型:操作,支持单个或多个操作权限,支持通配符号*,通配符号表示所有。例如 s3:GetObject ,表示获取对象 |
 * | Resource  | Resource(资源)策略所作用的资源,支持通配符号*,通配符号表示所有。在JSON视图中,不带Resource表示对所有资源生效。Resource支持以下字符:-_0-9a-zA-Z*./\,如果Resource中包含不支持的字符,请采用通配符号*。例如:arn:aws:s3:::my-bucketname/myobject*\,表示minio中my-bucketname/myobject目录下所有对象文件。 |
 * | Condition | Condition(条件)您可以在创建自定义策略时,通过Condition元素来控制策略何时生效。Condition包括条件键和运算符,条件键表示策略语句的Condition元素,分为全局级条件键和服务级条件键。全局级条件键(前缀为g:)适用于所有操作,服务级条件键(前缀为服务缩写,如obs:)仅适用于对应服务的操作。运算符与条件键一起使用,构成完整的条件判断语句。 |
 * @since 2023/3/16 15:28
 */
@Slf4j
@Data
@AllArgsConstructor
@NoArgsConstructor
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public class OssPolicy {
    /**
     * 标识策略的版本号,Minio中一般为"**2012-10-17**"
     */
    @JsonProperty("Version")
    private String version = "2012-10-17";

    /**
     * 策略授权语句,描述策略的详细信息,包含
     * Effect(效果)
     * Action(动作)
     * Principal(用户)
     * Resource(资源)
     * 和Condition(条件)。
     * 其中Condition为可选
     */
    @JsonProperty("Statement")
    private Statement[] statement;

    /**
     * 获取公共读的权限json字符串
     *
     * @param bucketName 桶名称
     * @return 公共读的权限json字符串
     */
    public static String getReadOnlyJsonPolicy(String bucketName) {
        return "{\n" +
                "  \"Version\": \"2012-10-17\",\n" +
                "  \"Statement\": [\n" +
                "    {\n" +
                "      \"Effect\": \"Allow\",\n" +
                "      \"Principal\": {\n" +
                "        \"AWS\": [\n" +
                "          \"*\"\n" +
                "        ]\n" +
                "      },\n" +
                "      \"Action\": [\n" +
                "        \"s3:GetBucketLocation\",\n" +
                "        \"s3:ListBucket\"\n" +
                "      ],\n" +
                "      \"Resource\": [\n" +
                "        \"arn:aws:s3:::" + bucketName + "\"\n" +
                "      ]\n" +
                "    },\n" +
                "    {\n" +
                "      \"Effect\": \"Allow\",\n" +
                "      \"Principal\": {\n" +
                "        \"AWS\": [\n" +
                "          \"*\"\n" +
                "        ]\n" +
                "      },\n" +
                "      \"Action\": [\n" +
                "        \"s3:GetObject\"\n" +
                "      ],\n" +
                "      \"Resource\": [\n" +
                "        \"arn:aws:s3:::" + bucketName + "/*\"\n" +
                "      ]\n" +
                "    }\n" +
                "  ]\n" +
                "}";
    }

    /**
     * 获取公共写的权限json字符串
     *
     * @param bucketName 桶名称
     * @return 公共写的权限json字符串
     */
    public static String getWriteOnlyJsonPolicy(String bucketName) {
        return "{\n" +
                "  \"Version\": \"2012-10-17\",\n" +
                "  \"Statement\": [\n" +
                "    {\n" +
                "      \"Effect\": \"Allow\",\n" +
                "      \"Principal\": {\n" +
                "        \"AWS\": [\n" +
                "          \"*\"\n" +
                "        ]\n" +
                "      },\n" +
                "      \"Action\": [\n" +
                "        \"s3:GetBucketLocation\",\n" +
                "        \"s3:ListBucketMultipartUploads\"\n" +
                "      ],\n" +
                "      \"Resource\": [\n" +
                "        \"arn:aws:s3:::" + bucketName + "\"\n" +
                "      ]\n" +
                "    },\n" +
                "    {\n" +
                "      \"Effect\": \"Allow\",\n" +
                "      \"Principal\": {\n" +
                "        \"AWS\": [\n" +
                "          \"*\"\n" +
                "        ]\n" +
                "      },\n" +
                "      \"Action\": [\n" +
                "        \"s3:AbortMultipartUpload\",\n" +
                "        \"s3:DeleteObject\",\n" +
                "        \"s3:ListMultipartUploadParts\",\n" +
                "        \"s3:PutObject\"\n" +
                "      ],\n" +
                "      \"Resource\": [\n" +
                "        \"arn:aws:s3:::" + bucketName + "/*\"\n" +
                "      ]\n" +
                "    }\n" +
                "  ]\n" +
                "}";
    }

    /**
     * 获取公共读写的权限json字符串
     *
     * @param bucketName 桶名称
     * @return 公共读写的权限json字符串
     */
    public static String getReadWriteJsonPolicy(String bucketName) {
        return "{\n" +
                "  \"Version\": \"2012-10-17\",\n" +
                "  \"Statement\": [\n" +
                "    {\n" +
                "      \"Effect\": \"Allow\",\n" +
                "      \"Principal\": {\n" +
                "        \"AWS\": [\n" +
                "          \"*\"\n" +
                "        ]\n" +
                "      },\n" +
                "      \"Action\": [\n" +
                "        \"s3:GetBucketLocation\",\n" +
                "        \"s3:ListBucket\",\n" +
                "        \"s3:ListBucketMultipartUploads\"\n" +
                "      ],\n" +
                "      \"Resource\": [\n" +
                "        \"arn:aws:s3:::" + bucketName + "\"\n" +
                "      ]\n" +
                "    },\n" +
                "    {\n" +
                "      \"Effect\": \"Allow\",\n" +
                "      \"Principal\": {\n" +
                "        \"AWS\": [\n" +
                "          \"*\"\n" +
                "        ]\n" +
                "      },\n" +
                "      \"Action\": [\n" +
                "        \"s3:ListMultipartUploadParts\",\n" +
                "        \"s3:PutObject\",\n" +
                "        \"s3:AbortMultipartUpload\",\n" +
                "        \"s3:DeleteObject\",\n" +
                "        \"s3:GetObject\"\n" +
                "      ],\n" +
                "      \"Resource\": [\n" +
                "        \"arn:aws:s3:::" + bucketName + "/*\"\n" +
                "      ]\n" +
                "    }\n" +
                "  ]\n" +
                "}";
    }


    /**
     * 需要对返回值判空
     *
     * @param inputStream 输入流
     * @return 策略文件
     */
    public static String getOssPolicyByReadJsonFile(InputStream inputStream) {
        try (BufferedInputStream bis = new BufferedInputStream(inputStream)) {
            return IoUtil.readUtf8(bis);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }


    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    @JsonInclude(JsonInclude.Include.NON_EMPTY)
    private static class Statement {
        /**
         * Effect(效果)作用包含两种:Allow(允许)和Deny(拒绝),
         * 系统预置策略仅包含允许的授权语句,
         * 自定义策略中可以同时包含允许和拒绝的授权语句,
         * 当策略中既有允许又有拒绝的授权语句时,
         * 遵循Deny优先的原则。
         */
        @JsonProperty("Effect")
        private String effect = "Allow";

        @JsonProperty("Principal")
        private Principal principal;

        /**
         * Action(动作)对资源的具体操作权限,
         * 格式为:服务名:资源类型:操作,支持单个或多个操作权限,支持通配符号*,通配符号表示所有。
         * 例如 s3:GetObject ,表示获取对象
         */
        @JsonProperty("Action")
        private String[] actions;

        /**
         * Resource(资源)策略所作用的资源,支持通配符号*,通配符号表示所有。
         * 在JSON视图中,不带Resource表示对所有资源生效。
         * Resource支持以下字符:-_0-9a-zA-Z*./\,如果Resource中包含不支持的字符,请采用通配符号*。
         * 例如:arn:aws:s3:::my-bucketname/myobject*\,表示minio中my-bucketname/myobject目录下所有对象文件。
         */
        @JsonProperty("Resource")
        private String[] resources;

        /**
         * Condition(条件)您可以在创建自定义策略时,通过Condition元素来控制策略何时生效。
         * Condition包括条件键和运算符,条件键表示策略语句的Condition元素,分为全局级条件键和服务级条件键。
         * 全局级条件键(前缀为g:)适用于所有操作,服务级条件键(前缀为服务缩写,如obs:)仅适用于对应服务的操作。
         * 运算符与条件键一起使用,构成完整的条件判断语句。
         */
        @JsonProperty("Condition")
        private String condition;
    }

    @NoArgsConstructor
    @AllArgsConstructor
    @Data
    @JsonInclude(JsonInclude.Include.NON_EMPTY)
    private static class Principal {
        @JsonProperty("AWS")
        private String[] aws;
    }


    public static void main(String[] args) throws JsonProcessingException {
        //System.out.println(DefaultPolicy.READ_ONLY.getPolicyJson());


        /*ObjectMapper objectMapper = new ObjectMapper();
        OssPolicy ossPolicy = new OssPolicy();
        ossPolicy.setVersion("2012-10-17");
        Statement statement = new Statement();
        statement.setEffect("Allow");
        String[] actions1 = {"admin:*"};
        statement.setActions(actions1);

        Statement statement2 = new Statement();
        statement2.setEffect("Allow");
        String[] actions2 = {"s3:*"};
        String[] resource2 = {"arn:aws:s3:::*"};
        statement2.setActions(actions2);
        statement2.setResources(resource2);

        Statement[] statements = {statement, statement2};
        ossPolicy.setStatement(statements);


        String jsonStr = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(ossPolicy);
        System.out.println(jsonStr);*/
    }
}

工具类

FileTypeUtil

@Slf4j
public final class FileTypeUtil {

    private static final Map<String, List<String>> MIME_TYPE_MAP;

    static {
        MIME_TYPE_MAP = new HashMap<>();
        try {
            SAXReader saxReader = new SAXReader();
            Document document = saxReader.read(Thread.currentThread().getContextClassLoader().getResourceAsStream(
                    "mime/mime-types.xml"));

            Element rootElement = document.getRootElement();
            List<Element> mimeTypeElements = rootElement.elements("mime-type");
            for (Element mimeTypeElement : mimeTypeElements) {
                String type = mimeTypeElement.attributeValue("type");
                List<Element> globElements = mimeTypeElement.elements("glob");
                List<String> fileTypeList = new ArrayList<>(globElements.size());
                for (Element globElement : globElements) {
                    String fileType = globElement.getTextTrim();
                    fileTypeList.add(fileType);
                }
                MIME_TYPE_MAP.put(type, fileTypeList);
            }
        } catch (DocumentException e) {
            log.error("", e);
        }

    }

    private FileTypeUtil() {
    }


    /**
     * 获取文件的MimeType
     *
     * @param inputStream 文件流
     * @param fileName    文件名
     * @param fileSize    文件字节大小
     * @return 文件的MimeType
     */
    public static String getFileMimeType(InputStream inputStream, String fileName, Long fileSize) {
        AutoDetectParser parser = new AutoDetectParser();
        parser.setParsers(new HashMap<>());
        Metadata metadata = new Metadata();

        // 设置资源名称
        if (!ObjectUtils.isEmpty(fileName)) {
            metadata.set(TikaCoreProperties.RESOURCE_NAME_KEY, fileName);
        }

        // 设置资源大小
        if (!ObjectUtils.isEmpty(fileSize)) {
            metadata.set(Metadata.CONTENT_LENGTH, Long.toString(fileSize));
        }
        try (InputStream stream = inputStream) {
            parser.parse(stream, new DefaultHandler(), metadata, new ParseContext());
        } catch (IOException | SAXException | TikaException e) {
            log.error("", e);
            throw new IllegalArgumentException("文件的MimeType类型解析失败,原因:" + e.getMessage());
        }
        return metadata.get(HttpHeaders.CONTENT_TYPE);
    }

    /**
     * 获取文件的MimeType
     *
     * @param inputStream inputStream
     * @return 文件的MimeType
     */
    public static String getFileMimeType(InputStream inputStream) throws IllegalArgumentException {
        return getFileMimeType(inputStream, null, null);
    }

    /**
     * 获取文件的真实类型, 全为小写
     *
     * @param inputStream inputStream
     * @return String
     */
    public static List<String> getFileRealTypeList(InputStream inputStream, String fileName, Long fileSize) {
        String fileMimeType = getFileMimeType(inputStream, fileName, fileSize);
        log.info("fileMimeType:{}", fileMimeType);
        return getFileRealTypeList(fileMimeType);
    }


    /**
     * 获取文件的真实类型, 全为小写
     *
     * @param inputStream inputStream
     * @return String
     * @throws IOException IOException
     */
    public static List<String> getFileRealTypeList(InputStream inputStream) throws IOException {
        return getFileRealTypeList(inputStream, null, null);
    }


    /**
     * 根据文件的mime类型获取文件的真实扩展名集合
     *
     * @param mimeType 文件的mime 类型
     * @return 文件的扩展名集合
     */
    public static List<String> getFileRealTypeList(String mimeType) {
        if (ObjectUtils.isEmpty(mimeType)) {
            return Collections.emptyList();
        }

        List<String> fileTypeList = MIME_TYPE_MAP.get(mimeType.replace(" ", ""));
        if (fileTypeList == null) {
            log.info("mimeType:{}, FileTypeList is null", mimeType);
            return Collections.emptyList();
        }
        return fileTypeList;
    }
}

Md5Util

@Slf4j
public final class Md5Util {
    private static final int BUFFER_SIZE = 8 * 1024;

    private static final char[] HEX_CHARS =
            {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};

    private Md5Util() {
    }

    /**
     * 计算字节数组的md5
     *
     * @param bytes bytes
     * @return 文件流的md5
     */
    public static String calculateMd5(byte[] bytes) {
        try {
            MessageDigest md5MessageDigest = MessageDigest.getInstance("MD5");
            return encodeHex(md5MessageDigest.digest(bytes));
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalArgumentException("no md5 found");
        }
    }

    /**
     * 计算文件的输入流
     *
     * @param inputStream inputStream
     * @return 文件流的md5
     */
    public static String calculateMd5(InputStream inputStream) {
        try {
            MessageDigest md5MessageDigest = MessageDigest.getInstance("MD5");
            try (BufferedInputStream bis = new BufferedInputStream(inputStream);
                 DigestInputStream digestInputStream = new DigestInputStream(bis, md5MessageDigest)) {

                final byte[] buffer = new byte[BUFFER_SIZE];

                while (digestInputStream.read(buffer) > 0) {
                    // 获取最终的MessageDigest
                    md5MessageDigest = digestInputStream.getMessageDigest();
                }

                return encodeHex(md5MessageDigest.digest());
            } catch (IOException ioException) {
                log.error("", ioException);
                throw new IllegalArgumentException(ioException.getMessage());
            }
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalArgumentException("no md5 found");
        }
    }

    /**
     * 获取字符串的MD5值
     *
     * @param input 输入的字符串
     * @return md5
     */
    public static String calculateMd5(String input) {
        try {
            // 拿到一个MD5转换器(如果想要SHA1参数,可以换成SHA1)
            MessageDigest md5MessageDigest = MessageDigest.getInstance("MD5");
            byte[] inputByteArray = input.getBytes(StandardCharsets.UTF_8);
            md5MessageDigest.update(inputByteArray);

            // 转换并返回结果,也是字节数组,包含16个元素
            byte[] resultByteArray = md5MessageDigest.digest();
            // 将字符数组转成字符串返回
            return encodeHex(resultByteArray);
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalArgumentException("md5 not found");
        }
    }

    /**
     * 转成的md5值为全小写
     *
     * @param bytes bytes
     * @return 全小写的md5值
     */
    private static String encodeHex(byte[] bytes) {
        char[] chars = new char[32];
        for (int i = 0; i < chars.length; i = i + 2) {
            byte b = bytes[i / 2];
            chars[i] = HEX_CHARS[(b >>> 0x4) & 0xf];
            chars[i + 1] = HEX_CHARS[b & 0xf];
        }
        return new String(chars);
    }
}
	

MediaType

public class MediaType implements Serializable {
    private static final long serialVersionUID = 560696828359220276L;
    public static final String ALL_VALUE = "*/*";
}

MinioTemplate

@Slf4j
public class MinioTemplate {
    /**
     * MinIO 客户端
     */
    private MinioClient minioClient;


    /**
     * MinIO 配置类
     */
    @Autowired
    private OSSProperties ossProperties;


    /**
     * 初始化操作
     * 初始化MinioClient 客户端
     * 并初始化默认桶
     */
    @PostConstruct
    public void init() {
        minioClient = MinioClient.builder()
                .endpoint(ossProperties.getEndpoint())
                .credentials(ossProperties.getAccessKey(), ossProperties.getSecretKey())
                .build();

        String defaultBucketName = ossProperties.getBucketName();
        if (bucketExists(defaultBucketName)) {
            log.info("默认存储桶:{} 已存在", defaultBucketName);
        } else {
            log.info("创建默认存储桶:{}", defaultBucketName);
            makeBucket(ossProperties.getBucketName());
        }
    }

    /**
     * 获取默认的桶
     *
     * @return default BucketName
     */
    public String getDefaultBucketName() {
        return ossProperties.getBucketName();
    }

    /**
     * 查询所有存储桶
     *
     * @return Bucket 集合
     */
    @SneakyThrows
    public List<Bucket> listBuckets() {
        return minioClient.listBuckets();
    }

    /**
     * 桶是否存在
     *
     * @param bucketName 桶名
     * @return 是否存在
     */
    @SneakyThrows
    public boolean bucketExists(String bucketName) {
        return minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
    }

    /**
     * 创建存储桶
     *
     * @param bucketName 桶名
     */
    @SneakyThrows
    public synchronized void makeBucket(String bucketName) {
        if (!bucketExists(bucketName)) {
            minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
        }
    }

    /**
     * 设置桶的存储权限
     *
     * @param bucketName 桶的名称
     * @param config     桶的权限配置,有四种,一是私有,一个是公共读,一个是公共读写,一个是公共写
     */
    @SneakyThrows
    public void setBucketPolicy(String bucketName, String config) {
        minioClient.setBucketPolicy(SetBucketPolicyArgs.builder()
                .config(config)
                .bucket(bucketName)
                .build());
    }

    /**
     * 删除一个空桶 如果存储桶存在对象不为空时,删除会报错。
     *
     * @param bucketName 桶名
     */
    @SneakyThrows
    public void removeBucket(String bucketName) {
        removeBucket(bucketName, false);
        minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());
    }

    /**
     * 删除一个桶 根据桶是否存在数据进行不同的删除
     * 桶为空时直接删除
     * 桶不为空时先删除桶中的数据,然后再删除桶
     *
     * @param bucketName 桶名
     */
    @SneakyThrows
    public void removeBucket(String bucketName, boolean bucketNotNull) {
        if (bucketNotNull) {
            deleteBucketAllObject(bucketName);
        }
        minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());
    }

    /**
     * 上传文件
     *
     * @param inputStream      流
     * @param originalFileName 原始文件名
     * @param bucketName       桶名
     * @return ObjectWriteResponse
     */
    @SneakyThrows
    public OssFile putObject(InputStream inputStream, String bucketName, String originalFileName) {
        String uuidFileName = generateFileInMinioName(originalFileName);
        try {
            if (ObjectUtils.isEmpty(bucketName)) {
                bucketName = ossProperties.getBucketName();
            }
            minioClient.putObject(
                    PutObjectArgs.builder()
                            .bucket(bucketName)
                            .object(uuidFileName)
                            .stream(inputStream, inputStream.available(), -1)
                            .build());
            return new OssFile(uuidFileName, originalFileName);
        } finally {
            if (inputStream != null) {
                inputStream.close();
            }
        }
    }

    @SneakyThrows
    public void uploadObject(String bucketName, String objectName, String filePath) {
        minioClient.uploadObject(UploadObjectArgs.builder()
                .bucket(bucketName)
                .object(objectName)
                .filename(filePath)
                .build());
    }

    /**
     * 删除桶中所有的对象
     *
     * @param bucketName 桶对象
     */
    @SneakyThrows
    public void deleteBucketAllObject(String bucketName) {
        List<String> list = listObjectNames(bucketName);
        if (!list.isEmpty()) {
            for (String objectName : list) {
                deleteObject(bucketName, objectName);
            }
        }
    }

    @SneakyThrows
    public void deleteFolder(String bucketName, String folder) {
        Iterable<Result<Item>> results = listObjects(bucketName, folder, true);
        // 先删除子目录,最后再删除父目录
        for (Result<Item> result : results) {
            deleteObject(bucketName, result.get().objectName());
        }
        deleteObject(bucketName, folder);
    }

    /**
     * 查询桶中所有的对象名
     *
     * @param bucketName 桶名
     * @return objectNames
     */
    @SneakyThrows
    public List<String> listObjectNames(String bucketName) {
        List<String> objectNameList = new ArrayList<>();
        if (bucketExists(bucketName)) {
            Iterable<Result<Item>> results = listObjects(bucketName, true);
            for (Result<Item> result : results) {
                String objectName = result.get().objectName();
                objectNameList.add(objectName);
            }
        }
        return objectNameList;
    }


    /**
     * 删除一个对象
     *
     * @param bucketName 桶名
     * @param objectName 对象名
     */
    @SneakyThrows
    public void deleteObject(String bucketName, String objectName) {
        minioClient.removeObject(RemoveObjectArgs.builder()
                .bucket(bucketName)
                .object(objectName)
                .build());
    }

    /**
     * 上传分片文件
     *
     * @param inputStream 流
     * @param objectName  存入桶中的对象名
     * @param bucketName  桶名
     * @return ObjectWriteResponse
     */
    @SneakyThrows
    public OssFile putChunkObject(InputStream inputStream, String bucketName, String objectName) {
        try {
            minioClient.putObject(
                    PutObjectArgs.builder()
                            .bucket(bucketName)
                            .object(objectName)
                            .stream(inputStream, inputStream.available(), -1)
                            .build());
            return new OssFile(objectName, objectName);
        } finally {
            if (inputStream != null) {
                inputStream.close();
            }
        }
    }

    /**
     * 返回临时带签名、Get请求方式的访问URL
     *
     * @param bucketName 桶名
     * @param filePath   Oss文件路径
     * @return 临时带签名、Get请求方式的访问URL
     */
    @SneakyThrows
    public String getPresignedObjectUrl(String bucketName, String filePath) {
        return minioClient.getPresignedObjectUrl(
                GetPresignedObjectUrlArgs.builder()
                        .method(Method.GET)
                        .bucket(bucketName)
                        .object(filePath)
                        .build());
    }

    /**
     * 返回临时带签名、过期时间为1天的PUT请求方式的访问URL
     *
     * @param bucketName  桶名
     * @param filePath    Oss文件路径
     * @param queryParams 查询参数
     * @return 临时带签名、过期时间为1天的PUT请求方式的访问URL
     */
    @SneakyThrows
    public String getPresignedObjectUrl(String bucketName, String filePath, Map<String, String> queryParams) {
        return minioClient.getPresignedObjectUrl(
                GetPresignedObjectUrlArgs.builder()
                        .method(Method.PUT)
                        .bucket(bucketName)
                        .object(filePath)
                        .expiry(1, TimeUnit.DAYS)
                        .extraQueryParams(queryParams)
                        .build());
    }


    /**
     * GetObject接口用于获取某个文件(Object)。此操作需要对此Object具有读权限。
     *
     * @param bucketName 桶名
     * @param objectName 文件路径
     */
    @SneakyThrows
    public InputStream getObject(String bucketName, String objectName) {
        return minioClient.getObject(
                GetObjectArgs.builder().bucket(bucketName).object(objectName).build());
    }


    /**
     * GetObject接口用于获取某个文件(Object)。此操作需要对此Object具有读权限。
     *
     * @param bucketName 桶名
     * @param objectName 文件路径
     */
    @SneakyThrows
    public StatObjectResponse getObjectInfo(String bucketName, String objectName) {
        return minioClient.statObject(StatObjectArgs.builder()
                .bucket(bucketName)
                .object(objectName)
                .build());
    }


    /**
     * GetObject接口用于获取某个文件(Object)。此操作需要对此Object具有读权限。
     *
     * @param bucketName 桶名
     * @param objectName 文件路径
     * @param offset     截取流的开始位置
     * @param length     截取长度
     */
    @SneakyThrows
    public InputStream getObject(String bucketName, String objectName, Long offset, Long length) {
        return minioClient.getObject(
                GetObjectArgs.builder().bucket(bucketName).object(objectName).offset(offset).length(length).build());
    }


    /**
     * 查询桶的对象信息
     *
     * @param bucketName 桶名
     * @param recursive  是否递归查询
     * @return 桶的对象信息
     */
    @SneakyThrows
    public Iterable<Result<Item>> listObjects(String bucketName, boolean recursive) {
        return minioClient.listObjects(
                ListObjectsArgs.builder().bucket(bucketName).recursive(recursive).build());
    }

    /**
     * 查询桶的对象信息
     *
     * @param bucketName 桶名
     * @param prefix     指定的前缀名称
     * @param recursive  是否递归查询
     * @return 桶的对象信息
     */
    @SneakyThrows
    public Iterable<Result<Item>> listObjects(String bucketName, String prefix, boolean recursive) {
        return minioClient.listObjects(ListObjectsArgs.builder()
                .bucket(bucketName)
                .prefix(prefix)
                .recursive(recursive)
                .build());
    }

    /**
     * 获取带签名的临时上传元数据对象,前端可获取后,直接上传到Minio
     *
     * @param bucketName 桶名称
     * @param fileName   文件名
     * @return Map<String, String>
     */
    @SneakyThrows
    public Map<String, String> getPresignedPostFormData(String bucketName, String fileName) {
        // 为存储桶创建一个上传策略,过期时间为7天
        PostPolicy policy = new PostPolicy(bucketName, ZonedDateTime.now().plusDays(1));
        // 设置一个参数key,值为上传对象的名称
        policy.addEqualsCondition("key", fileName);
        // 添加Content-Type,例如以"image/"开头,表示只能上传照片,这里吃吃所有
        policy.addStartsWithCondition("Content-Type", MediaType.ALL_VALUE);
        // 设置上传文件的大小 64kiB to 10MiB.
        //policy.addContentLengthRangeCondition(64 * 1024, 10 * 1024 * 1024);
        return minioClient.getPresignedPostFormData(policy);
    }


    public String generateFileInMinioName(String originalFilename) {
        return "files" + StrUtil.SLASH + DateUtil.format(new Date(), "yyyy-MM-dd") + StrUtil.SLASH + UUID.randomUUID() + StrUtil.UNDERLINE + originalFilename;
    }

    /**
     * 文件合并,将分块文件组成一个新的文件
     *
     * @param bucketName       合并文件生成文件所在的桶
     * @param fileName         原始文件名
     * @param sourceObjectList 分块文件集合
     * @return OssFile
     */
    @SneakyThrows
    public OssFile composeObject(String bucketName, String fileName, List<ComposeSource> sourceObjectList) {
        String filenameExtension = StringUtils.getFilenameExtension(fileName);
        String objectName = UUID.randomUUID() + "." + filenameExtension;
        minioClient.composeObject(ComposeObjectArgs.builder()
                .bucket(bucketName)
                .object(objectName)
                .sources(sourceObjectList)
                .build());

        String presignedObjectUrl = getPresignedObjectUrl(bucketName, fileName);
        return new OssFile(presignedObjectUrl, fileName);
    }


    /**
     * 文件合并,将分块文件组成一个新的文件
     *
     * @param bucketName       合并文件生成文件所在的桶
     * @param objectName       原始文件名
     * @param sourceObjectList 分块文件集合
     * @return OssFile
     */
    @SneakyThrows
    public OssFile composeObject(List<ComposeSource> sourceObjectList, String bucketName, String objectName) {
        minioClient.composeObject(ComposeObjectArgs.builder()
                .bucket(bucketName)
                .object(objectName)
                .sources(sourceObjectList)
                .build());
        String presignedObjectUrl = getPresignedObjectUrl(bucketName, objectName);
        return new OssFile(presignedObjectUrl, objectName);
    }

    /**
     * 文件合并,将分块文件组成一个新的文件
     *
     * @param originBucketName 分块文件所在的桶
     * @param targetBucketName 合并文件生成文件所在的桶
     * @param objectName       存储于桶中的对象名
     * @return OssFile
     */
    @SneakyThrows
    public OssFile composeObject(String originBucketName, String targetBucketName, String objectName) {

        Iterable<Result<Item>> results = listObjects(originBucketName, true);
        List<String> objectNameList = new ArrayList<>();
        for (Result<Item> result : results) {
            Item item = result.get();
            objectNameList.add(item.objectName());
        }


        if (ObjectUtils.isEmpty(objectNameList)) {
            throw new IllegalArgumentException(originBucketName + "桶中没有文件,请检查");
        }

        List<ComposeSource> composeSourceList = new ArrayList<>(objectNameList.size());
        // 对文件名集合进行升序排序
        objectNameList.sort((o1, o2) -> Integer.parseInt(o2) > Integer.parseInt(o1) ? -1 : 1);
        for (String object : objectNameList) {
            composeSourceList.add(ComposeSource.builder()
                    .bucket(originBucketName)
                    .object(object)
                    .build());
        }

        return composeObject(composeSourceList, targetBucketName, objectName);
    }

    /**
     * 将Bucket指定目录下的文件合并,将分块文件组成一个新的文件
     *
     * @param bucketName 分块文件所在的桶
     * @param folder     对象的前缀名
     * @param objectName 存储于桶中的对象名
     * @return OssFile
     */
    @SneakyThrows
    public OssFile composeObjectByObjectFolder(String bucketName, String folder, String objectName) {

        Iterable<Result<Item>> results = listObjects(bucketName, folder, true);
        List<String> objectNameList = new ArrayList<>();
        for (Result<Item> result : results) {
            Item item = result.get();
            objectNameList.add(item.objectName());
        }


        if (ObjectUtils.isEmpty(objectNameList)) {
            throw new IllegalArgumentException(bucketName + "/" + folder + "文件夹中没有文件,请检查");
        }

        List<ComposeSource> composeSourceList = new ArrayList<>(objectNameList.size());
        objectNameList = objectNameList.stream().map(objectNameHandler -> objectNameHandler.replace(folder, "").replace("/", "")).collect(Collectors.toList());
        // 对文件名集合进行升序排序
        objectNameList.sort((o1, o2) -> Integer.parseInt(o2) > Integer.parseInt(o1) ? -1 : 1);

        objectNameList = objectNameList.stream().map(objectNameHandler -> folder + objectNameHandler).collect(Collectors.toList());
        for (String object : objectNameList) {
            composeSourceList.add(ComposeSource.builder()
                    .bucket(bucketName)
                    .object(object)
                    .build());
        }

        return composeObject(composeSourceList, bucketName, objectName);
    }

    /**
     * 获取桶的存储策略
     *
     * @param bucket bucket
     * @return 桶的存储策略
     */
    @SneakyThrows
    public String getBucketPolicy(String bucket) {
        return minioClient.getBucketPolicy(GetBucketPolicyArgs.builder().bucket(bucket).build());
    }
}

文件分片上传与合并

MinioFileController

@RestController
@RequestMapping(value = "/file")
@Slf4j
@CrossOrigin // 允许跨域
public class MinioFileController {
   @Autowired
   private MinioService minioService;

    @RequestMapping(value = "/home")
    public ModelAndView homeUpload() {
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.setViewName("upload");
        return modelAndView;
    }

    /**
     * 根据文件大小和文件的md5校验文件是否存在
     * 暂时使用Redis实现,后续需要存入数据库
     * 实现秒传接口
     *
     * @param md5 文件的md5
     * @return 操作是否成功
     */
    @GetMapping(value = "/check")
    public Map<String, Object> checkFileExists(String md5) {
       return minioService.uploadCheck(md5);
    }


    /**
     * 文件上传,适合大文件,集成了分片上传
     */
    @PostMapping(value = "/upload")
    public Map<String, Object> upload(HttpServletRequest req) {
       return minioService.upload(req);

    }

    /**
     * 文件合并
     *
     * @param shardCount 分片总数
     * @param fileName   文件名
     * @param md5        文件的md5
     * @param fileType   文件类型
     * @param fileSize   文件大小
     * @return 分片合并的状态
     */
    @GetMapping(value = "/merge")
    public Map<String, Object> merge(Integer shardCount, String fileName, String md5, String fileType,
                                     Long fileSize) {
       return minioService.merge(shardCount, fileName, md5, fileType, fileSize);
    }
}


MinioService

public interface MinioService {

    /**
     * 文件上传前的检查,这是为了实现秒传接口
     *
     * @param md5 文件的md5
     * @return 文件是否上传过的元数据
     */
    Map<String, Object> uploadCheck(String md5);

    /**
     * 文件上传的核心功能
     *
     * @param req 请求
     * @return 上传结果的元数据
     */
    Map<String, Object> upload(HttpServletRequest req);

    /**
     * 分片文件合并的核心方法
     *
     * @param shardCount 分片数
     * @param fileName   文件名
     * @param md5        文件的md5值
     * @param fileType   文件类型
     * @param fileSize   文件大小
     * @return 合并成功的元数据
     */
    Map<String, Object> merge(Integer shardCount, String fileName, String md5, String fileType,
                              Long fileSize);

    /**
     * 视频播放的核心功能
     *
     * @param request    request
     * @param response   response
     * @param bucketName 视频文件所在的桶
     * @param objectName 视频文件名
     */
    void videoPlay(HttpServletRequest request, HttpServletResponse response,
                   String bucketName,
                   String objectName);
}

MinioServiceImpl
@Slf4j
@Service
public class MinioServiceImpl implements MinioService {

    /**
     * 存储视频的元数据列表
     */
    private static final String OBJECT_INFO_LIST = "com:minio:media:objectList";

    /**
     * 已上传文件的md5列表
     */
    private static final String MD5_KEY = "com:minio:file:md5List";

    @Autowired
    private MinioTemplate minioTemplate;

    @Autowired
    private ObjectMapper objectMapper;

    @Resource(name = "jsonRedisTemplate")
    private RedisTemplate<String, Serializable> redisTemplate;

    /**
     * 文件上传前的检查,这是为了实现秒传接口
     *
     * @param md5 文件的md5
     * @return 文件是否上传过的元数据
     */
    @Override
    public Map<String, Object> uploadCheck(String md5) {
        Map<String, Object> resultMap = new HashMap<>();
        if (ObjectUtils.isEmpty(md5)) {
            resultMap.put("status", StatusCode.PARAM_ERROR.getCode());
            return resultMap;
        }
        // 先从Redis中查询
        String url = (String) redisTemplate.boundHashOps(MD5_KEY).get(md5);

        // 文件不存在
        if (ObjectUtils.isEmpty(url)) {
            resultMap.put("status", StatusCode.NOT_FOUND.getCode());
            return resultMap;
        }

        resultMap.put("status", StatusCode.SUCCESS.getCode());
        resultMap.put("url", url);
        // 文件已经存在了
        return resultMap;
    }

    /**
     * 文件上传的核心功能
     *
     * @param req 请求
     * @return 上传结果的元数据
     */
    @Override
    public Map<String, Object> upload(HttpServletRequest req) {
        Map<String, Object> map = new HashMap<>();

        MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) req;

        // 获得文件分片数据
        MultipartFile file = multipartRequest.getFile("data");

        // 上传过程中出现异常,状态码设置为50000
        if (file == null) {
            map.put("status", StatusCode.FAILURE.getCode());
            return map;
        }
        // 分片第几片
        int index = Integer.parseInt(multipartRequest.getParameter("index"));
        // 总片数
        int total = Integer.parseInt(multipartRequest.getParameter("total"));
        // 获取文件名
        String fileName = multipartRequest.getParameter("name");

        String md5 = multipartRequest.getParameter("md5");

        // 创建文件桶
        minioTemplate.makeBucket(md5);
        String objectName = String.valueOf(index);

        log.info("index: {}, total:{}, fileName:{}, md5:{}, objectName:{}", index, total, fileName, md5, objectName);

        // 当不是最后一片时,上传返回的状态码为20001
        if (index < total) {
            try {
                // 上传文件
                OssFile ossFile = minioTemplate.putChunkObject(file.getInputStream(), md5, objectName);
                log.info("{} upload success {}", objectName, ossFile);

                // 设置上传分片的状态
                map.put("status", StatusCode.ALONE_CHUNK_UPLOAD_SUCCESS.getCode());
                return map;
            } catch (Exception e) {
                e.printStackTrace();
                map.put("status", StatusCode.FAILURE.getCode());
                return map;
            }
        } else {
            // 为最后一片时状态码为20002
            try {
                // 上传文件
                minioTemplate.putChunkObject(file.getInputStream(), md5, objectName);

                // 设置上传分片的状态
                map.put("status", StatusCode.ALL_CHUNK_UPLOAD_SUCCESS.getCode());
                return map;
            } catch (Exception e) {
                e.printStackTrace();
                map.put("status", StatusCode.FAILURE.getCode());
                return map;
            }
        }
    }

    /**
     * 分片文件合并的核心方法
     *
     * @param shardCount 分片数
     * @param fileName   文件名
     * @param md5        文件的md5值
     * @param fileType   文件类型
     * @param fileSize   文件大小
     * @return 合并成功的元数据
     */
    @Override
    public Map<String, Object> merge(Integer shardCount, String fileName, String md5, String fileType, Long fileSize) {
        Map<String, Object> retMap = new HashMap<>();

        try {
            // 查询片数据
            List<String> objectNameList = minioTemplate.listObjectNames(md5);
            if (shardCount != objectNameList.size()) {
                // 失败
                retMap.put("status", StatusCode.FAILURE.getCode());
            } else {
                // 开始合并请求
                String targetBucketName = minioTemplate.getDefaultBucketName();
                String filenameExtension = StringUtils.getFilenameExtension(fileName);
                String fileNameWithoutExtension = UUID.randomUUID().toString();
                String objectName = fileNameWithoutExtension + "." + filenameExtension;
                minioTemplate.composeObject(md5, targetBucketName, objectName);

                log.info("桶:{} 中的分片文件,已经在桶:{},文件 {} 合并成功", md5, targetBucketName, objectName);

                // 合并成功之后删除对应的临时桶
                minioTemplate.removeBucket(md5, true);
                log.info("删除桶 {} 成功", md5);

                // 计算文件的md5
                String fileMd5 = null;
                try (InputStream inputStream = minioTemplate.getObject(targetBucketName, objectName)) {
                    fileMd5 = Md5Util.calculateMd5(inputStream);
                } catch (IOException e) {
                    log.error("", e);
                }

                // 计算文件真实的类型
                String type = null;
                List<String> typeList = new ArrayList<>();
                try (InputStream inputStreamCopy = minioTemplate.getObject(targetBucketName, objectName)) {
                    typeList.addAll(FileTypeUtil.getFileRealTypeList(inputStreamCopy, fileName, fileSize));
                } catch (IOException e) {
                    log.error("", e);
                }

                // 并和前台的md5进行对比
                if (!ObjectUtils.isEmpty(fileMd5) && !ObjectUtils.isEmpty(typeList) && fileMd5.equalsIgnoreCase(md5) && typeList.contains(fileType.toLowerCase(Locale.ENGLISH))) {
                    // 表示是同一个文件, 且文件后缀名没有被修改过
                    String url = minioTemplate.getPresignedObjectUrl(targetBucketName, objectName);

                    // 存入redis中
                    redisTemplate.boundHashOps(MD5_KEY).put(fileMd5, url);

                    // 成功
                    retMap.put("status", StatusCode.SUCCESS.getCode());
                } else {
                    log.info("非法的文件信息: 分片数量:{}, 文件名称:{}, 文件fileMd5:{}, 文件真实类型:{}, 文件大小:{}",
                            shardCount, fileName, fileMd5, typeList, fileSize);
                    log.info("非法的文件信息: 分片数量:{}, 文件名称:{}, 文件md5:{}, 文件类型:{}, 文件大小:{}",
                            shardCount, fileName, md5, fileType, fileSize);

                    // 并需要删除对象
                    minioTemplate.deleteObject(targetBucketName, objectName);
                    retMap.put("status", StatusCode.FAILURE.getCode());
                }
            }
        } catch (Exception e) {
            log.error("", e);
            // 失败
            retMap.put("status", StatusCode.FAILURE.getCode());
        }
        return retMap;
    }

    /**
     * 视频播放的核心功能
     *
     * @param request    request
     * @param response   response
     * @param bucketName 视频文件所在的桶
     * @param objectName 视频文件名
     */
    @Override
    public void videoPlay(HttpServletRequest request, HttpServletResponse response, String bucketName, String objectName) {
        // 设置响应报头
        // 需要查询redis
        String key = bucketName + ":" + objectName;
        Object obj = redisTemplate.boundHashOps(OBJECT_INFO_LIST).get(key);

        // 用于记录视频文件的元数据
        // 这里使用Redis的缓存作为优化
        MinioObject minioObject;
        if (obj == null) {
            StatObjectResponse objectInfo = null;
            try {
                objectInfo = minioTemplate.getObjectInfo(bucketName, objectName);
            } catch (Exception e) {
                log.error("{}中{}不存在: {}", bucketName, objectName, e.getMessage());
                response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
                response.setContentType("application/json;charset=utf-8");
                response.setStatus(HttpServletResponse.SC_NOT_FOUND);
                try {
                    response.getWriter().write(objectMapper.writeValueAsString(Result.error(StatusCode.NOT_FOUND)));
                } catch (IOException ex) {
                    throw new RuntimeException(ex);
                }
                return;
            }
            // 判断是否是视频,是否为mp4格式
            String filenameExtension = StringUtils.getFilenameExtension(objectName);
            if (ObjectUtils.isEmpty(filenameExtension) ||
                    !"mp4".equalsIgnoreCase(filenameExtension.toLowerCase(Locale.ENGLISH))) {
                throw new IllegalArgumentException("不支持的媒体类型, 文件名: " + objectName);
            }

            minioObject = new MinioObject();
            BeanUtils.copyProperties(objectInfo, minioObject);


            redisTemplate.boundHashOps(OBJECT_INFO_LIST).put(key, minioObject);
        } else {
            minioObject = (MinioObject) obj;
        }


        // 获取文件的长度
        long fileSize = minioObject.getSize();
        // Accept-Ranges: bytes
        response.setHeader("Accept-Ranges", "bytes");
        //pos开始读取位置;  last最后读取位置
        long startPos = 0;
        long endPos = fileSize - 1;
        String rangeHeader = request.getHeader("Range");
        if (!ObjectUtils.isEmpty(rangeHeader) && rangeHeader.startsWith("bytes=")) {

            try {
                // 情景一:RANGE: bytes=2000070- 情景二:RANGE: bytes=2000070-2000970
                String numRang = request.getHeader("Range").replaceAll("bytes=", "");
                if (numRang.startsWith("-")) {
                    endPos = fileSize - 1;
                    startPos = endPos - Long.parseLong(new String(numRang.getBytes(StandardCharsets.UTF_8), 1,
                            numRang.length() - 1)) + 1;
                } else if (numRang.endsWith("-")) {
                    endPos = fileSize - 1;
                    startPos = Long.parseLong(new String(numRang.getBytes(StandardCharsets.UTF_8), 0,
                            numRang.length() - 1));
                } else {
                    String[] strRange = numRang.split("-");
                    if (strRange.length == 2) {
                        startPos = Long.parseLong(strRange[0].trim());
                        endPos = Long.parseLong(strRange[1].trim());
                    } else {
                        startPos = Long.parseLong(numRang.replaceAll("-", "").trim());
                    }
                }

                if (startPos < 0 || endPos < 0 || endPos >= fileSize || startPos > endPos) {
                    // SC 要求的范围不满足
                    response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
                    return;
                }

                // 断点续传 状态码206
                response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
            } catch (NumberFormatException e) {
                log.error(request.getHeader("Range") + " is not Number!");
                startPos = 0;
            }
        }

        // 总共需要读取的字节
        long rangLength = endPos - startPos + 1;
        response.setHeader("Content-Range", String.format("bytes %d-%d/%d", startPos, endPos, fileSize));
        response.addHeader("Content-Length", String.valueOf(rangLength));
        //response.setHeader("Connection", "keep-alive");
        response.addHeader("Content-Type", "video/mp4");

        try (BufferedOutputStream bos = new BufferedOutputStream(response.getOutputStream());
             BufferedInputStream bis = new BufferedInputStream(
                     minioTemplate.getObject(bucketName, objectName, startPos, rangLength))) {
            IOUtils.copy(bis, bos);
        } catch (
                IOException e) {
            if (e instanceof ClientAbortException) {
                // ignore 这里就不要打日志,这里的异常原因是用户在拖拽视频进度造成的
            } else {
                log.error(e.getMessage());
            }
        }
    }
}

upload.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>

</head>
<body>
<script type="text/javascript" src="/js/jquery.js" th:src="@{/js/jquery.js}"></script>
<script type="text/javascript" src="/js/spark-md5.min.js" th:src="@{/js/spark-md5.min.js}"></script>
<script type="text/javascript" src="/js/base.js" th:src="@{/js/base.js}"></script>
<input type="file" name="file" id="file">
<script>
    /**
     * 分块计算文件的md5值
     * @param file 文件
     * @param chunkSize 分片大小
     * @returns Promise
     */
    function calculateFileMd5(file, chunkSize) {
        return new Promise((resolve, reject) => {
            let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
            let chunks = Math.ceil(file.size / chunkSize);
            let currentChunk = 0;
            let spark = new SparkMD5.ArrayBuffer();
            let fileReader = new FileReader();

            fileReader.onload = function (e) {
                spark.append(e.target.result);
                currentChunk++;
                if (currentChunk < chunks) {
                    loadNext();
                } else {
                    let md5 = spark.end();
                    resolve(md5);
                }
            };

            fileReader.onerror = function (e) {
                reject(e);
            };

            function loadNext() {
                let start = currentChunk * chunkSize;
                let end = start + chunkSize;
                if (end > file.size) {
                    end = file.size;
                }
                fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
            }

            loadNext();
        });
    }

    /**
     * 分块计算文件的md5值,默认分片大小为2097152(2M)
     * @param file 文件
     * @returns Promise
     */
    function calculateFileMd5ByDefaultChunkSize(file) {
        return calculateFileMd5(file, 2097152);
    }

    /**
     * 获取文件的后缀名
     */
    function getFileType(fileName) {
        return fileName.substr(fileName.lastIndexOf(".") + 1).toLowerCase();
    }

    // 文件选择之后就计算文件的md5值
    document.getElementById("file").addEventListener("change", function () {
        let file = this.files[0];
        calculateFileMd5ByDefaultChunkSize(file).then(e => {
            // 获取到文件的md5
            let md5 = e;
            checkMd5(md5, file)
        }).catch(e => {
            // 处理异常
            console.error(e);
        });
    });

    /**
     * 根据文件的md5值判断文件是否已经上传过了
     *
     * @param md5 文件的md5
     * @param file 准备上传的文件
     */
    function checkMd5(md5, file) {
        // 请求数据库,查询md5是否存在
        $.ajax({
            url: baseUrl + "/file/check",
            type: "GET",
            data: {
                md5: md5
            },
            async: true, //异步
            dataType: "json",
            success: function (msg) {
                console.log(msg);
                // 文件已经存在了,无需上传
                if (msg.status === 20000) {
                    console.log("文件已经存在了,无需上传")
                } else if (msg.status === 40004) {
                    // 文件不存在需要上传
                    console.log("文件不存在需要上传")
                    PostFile(file, 0, md5);
                } else {
                    console.log('未知错误');
                }
            }
        })
    }

    /**
     * 执行分片上传
     * @param file 上传的文件
     * @param i 第几分片,从0开始
     * @param md5 文件的md5值
     */
    function PostFile(file, i, md5) {
        let name = file.name,                           //文件名
            size = file.size,                           //总大小shardSize = 2 * 1024 * 1024,
            shardSize = 5 * 1024 * 1024,                //以5MB为一个分片,每个分片的大小
            shardCount = Math.ceil(size / shardSize);   //总片数
        if (i >= shardCount) {
            return;
        }

        let start = i * shardSize;
        let end = start + shardSize;
        let packet = file.slice(start, end);  //将文件进行切片
        /*  构建form表单进行提交  */
        let form = new FormData();
        form.append("md5", md5);// 前端生成uuid作为标识符传个后台每个文件都是一个uuid防止文件串了
        form.append("data", packet); //slice方法用于切出文件的一部分
        form.append("name", name);
        form.append("totalSize", size);
        form.append("total", shardCount); //总片数
        form.append("index", i + 1); //当前是第几片
        $.ajax({
            url: baseUrl + "/file/upload",
            type: "POST",
            data: form,
            //timeout:"10000",  //超时10秒
            async: true, //异步
            dataType: "json",
            processData: false, //很重要,告诉jquery不要对form进行处理
            contentType: false, //很重要,指定为false才能形成正确的Content-Type
            success: function (msg) {
                console.log(msg);
                /*  表示上一块文件上传成功,继续下一次  */
                if (msg.status === 20001) {
                    form = '';
                    i++;
                    PostFile(file, i, md5);
                } else if (msg.status === 50000) {
                    form = '';
                    /*  失败后,每2秒继续传一次分片文件  */
                    setInterval(function () {
                        PostFile(file, i, md5)
                    }, 2000);
                } else if (msg.status === 20002) {
                    merge(shardCount, name, md5, getFileType(file.name), file.size)
                    console.log("上传成功");
                } else {
                    console.log('未知错误');
                }
            }
        })
    }

    /**
     * 合并文件
     * @param shardCount 分片数
     * @param fileName 文件名
     * @param md5 文件md值
     * @param fileType 文件类型
     * @param fileSize 文件大小
     */
    function merge(shardCount, fileName, md5, fileType, fileSize) {
        $.ajax({
            url: baseUrl + "/file/merge",
            type: "GET",
            data: {
                shardCount: shardCount,
                fileName: fileName,
                md5: md5,
                fileType: fileType,
                fileSize: fileSize
            },
            // timeout:"10000",  //超时10秒
            async: true, //异步
            dataType: "json",
            success: function (msg) {
                console.log(msg);
            }
        })
    }
</script>

</body>
</html>


视频播放

VideoController

调用minio的播放方法

@RestController
@Slf4j
@RequestMapping(value = "/video")
@CrossOrigin
public class VideoController {

    @Autowired
    private MinioService minioService;

    /**
     * 支持分段读取视频流
     *
     * @param request    请求对象
     * @param response   响应对象
     * @param bucketName 视频所在桶的位置
     * @param objectName 视频的文件名
     */
    @GetMapping(value = "/play/{bucketName}/{objectName}")
    public void videoPlay(HttpServletRequest request, HttpServletResponse response,
                                  @PathVariable(value = "bucketName") String bucketName,
                                  @PathVariable(value = "objectName") String objectName) {
        minioService.videoPlay(request, response, bucketName, objectName);
    }

    @RequestMapping(value = "/home/{bucketName}/{objectName}")
    public ModelAndView videoHome( @PathVariable(value = "bucketName") String bucketName,
                                   @PathVariable(value = "objectName") String objectName) {
        ModelAndView modelAndView = new ModelAndView();

        modelAndView.addObject("bucketName", bucketName);
        modelAndView.addObject("objectName", objectName);
        modelAndView.setViewName("video");
        return modelAndView;
    }
}

video.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>ckplayer</title>
    <link rel="shortcut icon" href="#"/>
    <link type="text/css" rel="stylesheet" href="/ckplayer/css/ckplayer.css" th:href="@{/ckplayer/css/ckplayer.css}"/>

    <script type="text/javascript" src="/js/jquery.js" th:src="@{/js/jquery.js}"></script>

    <!--
        如果需要使用其它语言,请在此处引入相应的js,比如:<script type="text/javascript" src="ckplayer/language/en.js" charset="UTF-8"></script>
    -->
    <script type="text/javascript" src="/ckplayer/js/ckplayer.min.js" th:src="@{/ckplayer/js/ckplayer.min.js}"
            charset="UTF-8"></script>

    <script type="text/javascript" src="/js/base.js" th:src="@{/js/base.js}"></script>


</head>
<body>

<div class="video" style="width: 100%; height: 500px;max-width: 800px;">播放容器</div>


<p>官网:<a href="https://www.ckplayer.com" target="_blank">www.ckplayer.com</a></p>
<p>手册:<a href="https://www.ckplayer.com/manual/" target="_blank">www.ckplayer.com/manual/</a></p>
<p>社区:<a href="https://bbs.ckplayer.com/" target="_blank">bbs.ckplayer.com</a></p>
<p>全功能演示:<a href="https://www.ckplayer.com/demo.html" target="_blank">www.ckplayer.com/demo.html</a></p>
<p>控制示例:</p>
<p>
    <button type="button" onclick="player.play()">播放</button>
    <button type="button" onclick="player.pause()">暂停</button>
    <button type="button" onclick="player.seek(20)">跳转</button>
    <button type="button" onclick="player.volume(0.6)">修改音量</button>
    <button type="button" onclick="player.muted()">静音</button>
    <button type="button" onclick="player.exitMuted()">恢复音量</button>
    <button type="button" onclick="player.full()">全屏</button>
    <button type="button" onclick="player.webFull()">页面全屏</button>
    <button type="button" onclick="player.theatre()">剧场模式</button>
    <button type="button" onclick="player.exitTheatre()">退出剧场模式</button>
</p>
<p id="state"></p>
<p id="state2"></p>

</body>

<!--JS获取-->
<script type="text/javascript" th:inline="javascript">
    const bucketName = [[${bucketName}]];
    const objectName = [[${objectName}]];
</script>

<script>

    //调用开始
    let videoObject = {
        container: '.video',//视频容器的ID
        volume: 0.8,//默认音量,范围0-1
        video: 'http://localhost:18002/video/play/'+ bucketName + '/' + objectName,//视频地址
    };
    let player = new ckplayer(videoObject)//调用播放器并赋值给变量player
    /*
     * ===============================================================================================
     * 以上代码已完成调用演示,下方的代码是演示监听动作和外部控制的部分
     * ===============================================================================================
     * ===============================================================================================
     */
    player.play(function () {
        document.getElementById('state').innerHTML = '监听到播放';
    });
    player.pause(function () {
        document.getElementById('state').innerHTML = '监听到暂停';
    });
    player.volume(function (vol) {
        document.getElementById('state').innerHTML = '监听到音量改变:' + vol;
    });
    player.muted(function (b) {
        document.getElementById('state2').innerHTML = '监听到静音状态:' + b;
    });
    player.full(function (b) {
        document.getElementById('state').innerHTML = '监听到全屏状态:' + b;
    });
    player.ended(function () {
        document.getElementById('state').innerHTML = '监听到播放结束';
    });
</script>
</html>

测试

上传

访问:http://localhost:18002/file/home来上传文件
在这里插入图片描述

在这里插入图片描述
注意到文件的objectName是:9cabffc5-1812-43c1-bcd3-d97a96b0282b.mp4

在这里插入图片描述

播放1

访问路径:http://localhost:18002/video/home/minio-demo/9cabffc5-1812-43c1-bcd3-d97a96b0282b.mp4
在这里插入图片描述

播放2

但是上面是通过后端直接拿的流,应该直接向mino获取文件流数据,由于这个bucket是private不能直接访问,因此这里可以直接向后端拿到签名的url访问地址,前端可以直接使用这个地址播放

首先生成可访问的签名url

@Test
void contextLoads() throws Exception {
    String bucketName = "minio-demo";
    String filePath = "9cabffc5-1812-43c1-bcd3-d97a96b0282b.mp4";

    MinioClient minioClient = MinioClient.builder()
            .endpoint(ossProperties.getEndpoint())
            .credentials(ossProperties.getAccessKey(), ossProperties.getSecretKey())
            .build();
    String presignedObjectUrl = minioClient.getPresignedObjectUrl(
            GetPresignedObjectUrlArgs.builder()
                    .method(Method.GET)
                    .bucket(bucketName)
                    .object(filePath)
                    .expiry()
                    .build());
    System.out.println(presignedObjectUrl);
}

然后将url给到video标签(给到ckplayer也可以播放,已测试)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <video src="http://127.0.0.1:18000/minio-demo/9cabffc5-1812-43c1-bcd3-d97a96b0282b.mp4?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=qwiVxtzgeYbGSEZuV9ki%2F20240828%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20240828T044350Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=host&X-Amz-Signature=f83bdffdbc101e9973754545c110ffbe3a46c4920c418eb182fcc18914a0ea4c" controls>
</body>
</html>

在这里插入图片描述

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

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

相关文章

JVM堆内存空间(heap)

在Java程序运行时&#xff0c;系统运行过程中产生的大部分实例对象以及数组对象都会被放到堆中存储。 默认情况下&#xff0c;如果不通过参数强制指定堆空间大小&#xff0c;那么JVM会根据当前所在的平台进行自适应调整&#xff0c;起始大小默认为当前物理机器内存的1/64&…

我的第一个QT程序_创建项目_全程图解

创建项目 1.打开QT开发工具 2.点【创建项目】 在弹出的窗口的项目列表中选择【Application(Qt)】中间列选择 第一项 如上图。 3.输入项目名称和项目保存的路径 4.Build system下拉列表中有三个选项&#xff0c;虽然Qt6主推CMake&#xff0c;所以可以默认&#xff0c;直接下一步…

【用Java学习数据结构系列】震惊,二叉树原来是要这么学习的(二)

看到这句话的时候证明&#xff1a;此刻你我都在努力 加油陌生人 个人主页&#xff1a;Gu Gu Study 专栏&#xff1a;用Java学习数据结构系列 喜欢的一句话&#xff1a; 常常会回顾努力的自己&#xff0c;所以要为自己的努力留下足迹 喜欢的话可以点个赞谢谢了。 作者&#xff…

基于yolov8的玉米病害检测系统python源码+onnx模型+评估指标曲线+精美GUI界面

【算法介绍】 基于YOLOv8的玉米病害检测系统是一款利用前沿深度学习技术开发的智能农业工具。该系统以YOLOv8为核心算法&#xff0c;通过大量玉米病害图片的训练&#xff0c;能够精准识别玉米害虫病害。 该系统具备高效、准确的检测能力&#xff0c;支持图片、批量图片、视频…

7.整数反转

7.整数反转 给你一个 32 位的有符号整数 x &#xff0c;返回将 x 中的数字部分反转后的结果。 如果反转后整数超过 32 位的有符号整数的范围 [−231, 231 − 1] &#xff0c;就返回 0。 假设环境不允许存储 64 位整数&#xff08;有符号或无符号&#xff09;。 示例 1&#x…

Linux主机网络参数的设置—IP地址的作用和类型

网络参数管理 一.网络参数 主机名&#xff0c;IP地址&#xff0c;子网掩码&#xff0c;网关&#xff0c;DNS服务器地址 1.配置主机名 hostname命令来查看当前系统的主机名&#xff0c; hosnamectl set-hostname 修改centos7的主机名&#xff0c; 建议以FQDN的&#xff…

顶级 USB 恢复工具探讨:2024 -2025 年最佳 USB 数据恢复

在数字数据与物理文档一样重要的时代&#xff0c;丢失 USB 驱动器中的数据可能是一种令人恐慌的经历。无论是重要的工作文件、珍贵的照片还是重要文档&#xff0c;数据丢失都会以难以想象的方式让您倒退。值得庆幸的是&#xff0c;一些 USB 恢复工具旨在帮助您度过这些可怕的时…

【Delphi】一种生成透明 Icon 图标方法、原理

在程序开发中&#xff0c;我们会遇到制作程序的主图标&#xff0c;windows下程序的图标给是要求是ico格式&#xff0c;也就是常说的Icon。本文介绍一种Delphi利用windos API生成icon的方法。 在制作ico图标的时候&#xff0c;我们需要两幅bitmap图片&#xff0c;一幅我们称作掩…

单片机中的定时器:精确时间的掌控者

在单片机的世界里&#xff0c;定时器就像是一个精确的时间守护者&#xff0c;默默地为各种任务提供准确的时间基准。从简单的定时功能到复杂的实时控制系统&#xff0c;定时器都发挥着至关重要的作用。本文将深入探讨单片机中的定时器&#xff0c;包括其工作原理、应用场景以及…

华为OD机试真题 - 高效货运 - 暴力搜索(Java/Python/JS/C/C++ 2024 E卷 100分)

华为OD机试 2024E卷题库疯狂收录中,刷题点这里 专栏导读 本专栏收录于《华为OD机试真题(Java/Python/JS/C/C++)》。 刷的越多,抽中的概率越大,私信哪吒,备注华为OD,加入华为OD刷题交流群,每一题都有详细的答题思路、详细的代码注释、3个测试用例、为什么这道题采用XX…

JAVA开源项目 加油站管理系统 计算机毕业设计

本文项目编号 T 003 &#xff0c;文末自助获取源码 \color{red}{T003&#xff0c;文末自助获取源码} T003&#xff0c;文末自助获取源码 目录 一、系统介绍二、演示录屏三、启动教程四、功能截图五、文案资料5.1 选题背景5.2 国内外研究现状5.3 可行性分析 六、核心代码6.1 查…

CTFHub技能树-备份文件下载-网站源码

目录 法一&#xff1a;使用自动化工具扫描 方法二&#xff1a;使用dirsearch目录扫描器扫描 法一&#xff1a;使用自动化工具扫描 import requestsurl1 http://challenge-0e8fe706d11de65e.sandbox.ctfhub.com:10800/ # url为被扫描地址&#xff0c;后不加‘/’# 常见的网…

【论文分享】sIOPMP: Scalable and Efficient I/O Protection for TEEs 24‘ASPLOS

目录 AbstractINTRODUCTIONBACKGROUND and MOTIVATIONDMA AttackIOPMPRelated Work: Other I/O Isolation Mechanisms DESIGN OVERVIEWDesign GoalsPerformanceSecurityScalability Threat ModelPrivilege software attacksMalicious device attacks DETAILED DESIGNMulti-stag…

【C++】智能指针——auto_ptr,unique_ptr,shared_ptr

目录 auto_ptr unique_ptr shared_ptr 并发问题 循环引用问题 个人主页&#xff1a;传送门——>东洛的克莱斯韦克 智能指针的原理&#xff1a;传送门——>智能指针的原理 auto_ptr 使用方法参考官方文档 传送门——>auto_ptr文档 auto_ptr并不是一个优秀的智能…

在线将多张图片拼接起来图工具HTML源码

源码介绍 在线将多张图片拼接成一张图片&#xff0c;多图合一并导出下载。无需本地安装软件。 下载时&#xff0c;使用日期时间作为文件名&#xff0c;规避图片文件名相同造成的覆盖问题&#xff1b;也能省去一部覆盖确认操作 多语言支持 源码截图 源码下载 在线将多张图片…

Xilinx实现对数运算

简介 本章节实现任意底数和真数值的转换,设计中一般有两种实现方法: 1、在外部直接算好对数值,按照数值范围做个表,存在ram里,到时候查表。为了减少表深度,提高资源利用率,可以考虑去掉部分低位数值,损失一定的精度。 2、log10(x)=ln(x) * log10(e) , log10(e)是常数可…

电信500M宽带+AX210无线网卡测速

500M电信宽带&#xff0c;PC的Wifi模块是AX210 一、PC测速 2.4G Wifi 5G Wifi 有线网口 二、 手机端&#xff0c;小翼管家App测速 2.4G Wifi 5G Wifi 结论&#xff1a; 手机上网要快的话&#xff0c;还是要选择5G wifi

【Linux】用户和权限及实用操作------迅速了解用户和权限及其实用操作

目录 &#x1f354; Linux用户和权限 1.1 Linux 用户相关概念 1.2 用户权限 1.3 文件/文件夹权限的修改 &#x1f354; Linux实用操作 2.1 快捷键 2.2 软件安装/服务启动状态管理/创建软连接 yum install systemctl 对服务进行管理 ln 软连接 2.3 IP 和 主机名 2.4…

华为云征文 | 快速部署华为云Flexus X实例,开启您的云端之旅

需要了解 本文章主要讲述华为云Flexus X实例的介绍&#xff0c;以及在华为公有云平台&#xff0c;购买和配置华为云Flexus X实例的搭建指南选择合适的云服务器&#xff1a; 本文采用的是 华为云服务器 Flexus X 实例&#xff08;推荐使用&#xff09;共有有镜像&#xff1a; Hu…

关于C++的一些使用模版-初阶

一、泛型编程 如何实现一个通用的交换函数呢?,交换的值是两个类型不同的数据。 代码如下&#xff1a; #define _CRT_SECURE_NO_WARNINGS 1 #include<iostream>//如何实现一个通用的交换函数呢&#xff1f; void swap(int& left, int &right) {int tmp lef…