[Texto de mil palabras] Vue + SpringBoot realiza una segunda transmisión de archivos grandes, una reanudación de la transmisión de puntos de interrupción y un tutorial completo de carga de fragmentos (se proporciona el código fuente de Gitee)

Prólogo: Recientemente encontré un requisito en un proyecto real. Los clientes pueden cargar archivos relativamente grandes. Si se utiliza la solución de carga de archivos tradicional, puede haber algunos riesgos de seguridad, como alta presión del servidor, desperdicio de recursos e incluso desbordamiento de memoria. Por lo tanto, Para resolver una serie de El problema es que se necesita una nueva solución técnica para realizar la carga de archivos grandes, cuando estoy libre, consulto algunos tutoriales relacionados en Internet y finalmente lo resumo yo mismo. En este blog, Explicaré paso a paso cómo realizar archivos grandes. Para las tres funciones de segunda transmisión, transmisión de reanudación de punto de interrupción y fragmentación, se explicará cada fragmento de código. Al final del blog, proporcionaré el código fuente de Gitee para todos. descargar.

Tabla de contenido

1. ¿Por qué utilizar esta solución técnica? 

2. ¿Qué es la transmisión instantánea?

3. ¿Qué es reanudar desde el punto de interrupción? 

4. ¿Qué es la carga multiparte?

5. Proceso de carga

6. Construya el proyecto SpringBoot

6.1 Preparativos 

6.1.1 Importar dependencias de pom

6.1.2, archivo de configuración yml

6.1.3, archivo de configuración mybatis-config.xml

6.1.4, archivo SQL

6.2, clase constante 

6.2.1, constantes de código de estado de retorno común de HttpStatus

6.3, clase de entidad

6.3.1, encapsulación de resultados unificada AjaxResult

6.3.2, clase de segmento de archivo

6.3.3, clase de información de archivo

6.4, configuración global CORS entre dominios GlobalCorsConfig

6.5, capa de persistencia

6.5.1, archivo FileChunkMapper.xml

6.5.2, archivo FileChunkMapper

6.5.3, archivo FileInfoMapper.xml

6.5.4, archivo FileInfoMapper

6.6 Clase de servicio principal de carga de archivos (importante)

6.6.1, la lógica de la verificación de carga de archivos

6.6.2 Implementar la lógica central de la carga de fragmentos

6.6.3 Procesamiento de fragmentación de archivos

6.6.4, código completo

6.7, capa de solicitud FileController

7. Cree un proyecto de Vue

7.1 Preparativos

7.2, página InicioVer

7.2.1 Uso del componente de carga

7.2.2 Inicializar datos

7.2.3, método checkChunkUploadedByResponse (importante)

7.2.4, método parseTimeRemaining

7.2.5 Otros parámetros

7.2.6, método onFileAdded (importante)

7.2.7, método getFileMD5 (importante)

7.2.8, código completo

Ocho, ejecuta el proyecto.

8.1 Subir en pedazos

8.2 Currículum del punto de interrupción 

8.3 Segunda pasada 

9. Dirección del código fuente de Gitee

10. Resumen


1. ¿Por qué utilizar esta solución técnica? 

Si el front-end carga un archivo muy grande (como 1G) a la vez y no adopta soluciones técnicas como la fragmentación/punto de interrupción para reanudar la transmisión, se enfrentará principalmente a los siguientes peligros o problemas ocultos:

1. La velocidad de transmisión de la red es lenta.

El tiempo de carga es largo y la carga mínima completa del archivo debe ocupar un ancho de banda ascendente continuo y estable. Si las condiciones de la red no son buenas, la carga será muy lenta, lo que dañará la experiencia del usuario.

2. Si falla en el medio, debes volver a cargarlo.

Si el proceso de carga se interrumpe por motivos como la red, toda la transmisión fallará. Esto sólo requiere que el usuario vuelva a cargar el archivo completo, repitiendo la labor.

3. El servidor está bajo mucha presión.

El servidor necesita ocupar muchos recursos para procesar continuamente un archivo grande, lo que ejerce mucha presión sobre el rendimiento del servidor y puede afectar otros servicios.

4. Desperdicio de recursos de tráfico

Si un archivo grande se carga por completo de una vez, si el mismo archivo ya existe, consumirá repetidamente una gran cantidad de tráfico de red, lo que es un desperdicio de datos.

5. Es difícil darse cuenta del mensaje de progreso de carga.

El usuario no puede percibir el progreso de la carga y no sabe cuántos datos se han cargado si la carga falla.

Por tanto, para solucionar estos problemas es muy importante utilizar tecnologías como la fragmentación y la reanudación de la transmisión. Puede cargar bloques de datos en lotes, evitando la desventaja de cargarlos todos a la vez. Al mismo tiempo, combinado con métodos como la verificación y el registro de fragmentos cargados, puede hacer que todo el proceso de carga sea controlable, recuperable, ahorre tráfico y mejore en gran medida la eficiencia de la transmisión.

2. ¿Qué es la transmisión instantánea?

Utilizaré este proyecto para explicar la lógica de implementación de Miaochuan de una manera fácil de entender.

1. Cuando el cliente Vue carga un archivo, primero calcula el valor MD5 del archivo y luego envía el valor MD5 al servidor SpringBoot.

2. Después de que el servidor SpringBoot recibe el valor MD5 , utiliza MyBatis para consultar la base de datos MySQL y verificar si ya existe un archivo con el mismo valor MD5 .

3. Si existe, significa que el archivo se ha cargado y el servidor pregunta directamente qué fragmentos existen en el archivo desde la base de datos y lo devuelve al cliente.

4. Después de que el cliente obtenga la información de fragmentación del archivo, ensamblará directamente el archivo completo sin cargar el contenido real del archivo.

5. Si el valor MD5 no existe en la base de datos, significa que el archivo no se ha cargado y el servidor devolverá que el cliente necesita cargar el archivo completo.

6. Después de que el cliente cargue el archivo, el servidor agregará la relación correspondiente entre el archivo y MD 5 en la base de datos MySQL y almacenará la información de fragmentación del archivo.

7. La próxima vez que se cargue el mismo archivo, se puede utilizar el valor MD5 para cargarlo en segundos.

Entonces, el núcleo es usar la base de datos MySQL para registrar MD5 y la información de fragmentación de cada archivo . Consultar a través de MD5 durante la carga y MySQL puede juzgar si se permite la transmisión instantánea para evitar cargas repetidas del mismo archivo.

3. ¿Qué es reanudar desde el punto de interrupción? 

Luego explicaré el concepto de reanudar la transmisión con este proyecto de una manera fácil de entender.

1. Al cargar un archivo, el front-end Vue corta el archivo en varios fragmentos pequeños y carga un fragmento pequeño cada vez.

2. Cada vez que se carga un bloque pequeño, el backend SpringBoot registrará la información de este bloque pequeño, como el número de serie del bloque pequeño, el archivo MD5, el hash de contenido, etc. Se puede guardar en una base de datos MySQL.

3. Si se interrumpe la carga, el lado de Vue puede preguntarle a SpringBoot qué piezas pequeñas se han cargado.

4. SpringBoot consulta desde la base de datos y devuelve la pequeña información cargada a Vue.

5. Vue puede continuar cargando solo la pequeña parte que se interrumpe en el medio.

6. SpringBoot volverá a ensamblar estos pequeños bloques en un archivo completo de acuerdo con el número de serie de los pequeños bloques y el MD5 del archivo.

7. Si el archivo se carga nuevamente en el futuro, el valor MD5 se puede usar para saber que el archivo ya existe y la carga exitosa se devolverá directamente sin cargar el contenido real. De esta manera, al cargar sectores y registrar persistentemente la información de los sectores cargados, es posible reanudar la carga desde puntos de interrupción.

La clave es que SpringBoot necesita proporcionar una interfaz para registrar y obtener la información del segmento cargado. El lado de Vue necesita dividir y cargar en orden. Finalmente, SpringBoot reúne los archivos. En pocas palabras, la carga se interrumpe debido a la red y otras razones. Al registrar la cantidad de datos transmitidos, en Una solución técnica para continuar cargando los datos restantes después de una interrupción.

4. ¿Qué es la carga multiparte?

Al final, explicaré el concepto de carga de varias partes de una manera fácil de entender basada en este proyecto.

1. El propósito de cargar en partes es cortar un archivo grande en varias partes pequeñas para lograr cargas simultáneas y mejorar la velocidad de transmisión.

2. Los archivos grandes se pueden dividir según el tamaño del fragmento configurado (por ejemplo, un fragmento de 50 M).

3. El proyecto Vue carga cada división en el servidor SpringBoot en orden y luego carga cada pieza en orden.

4. Una vez que el servidor SpringBoot recibe el fragmento, puede almacenarlo temporalmente localmente y registrar la información característica del fragmento, como el número de serie del fragmento, el archivo MD5, etc., y escribirlo en la base de datos.

5. Después de cargar todos los fragmentos, SpringBoot los vuelve a ensamblar en los archivos originales completos en secuencia.

5. Proceso de carga

Esta es la lógica de procesamiento de cortes front-end que dibujé:

Esta es la lógica que dibujé para procesar cortes en el backend:

Esta es mi combinación lógica del proceso general de ejecución del proyecto: 

1. Cuando se cree, inicialice el componente de carga, especifique el tamaño del fragmento, el método de carga y otras configuraciones.

2. En el método onFileAdded, después de calcular el MD5 para el archivo seleccionado, llame a file.resume() para comenzar a cargar.

3. file.resume() primero envía una solicitud GET internamente para solicitar al servidor los fragmentos cargados del archivo.

4. El servidor devuelve un JSON que contiene una lista de fragmentos cargados.

5. El componente de carga llama a checkChunkUploadedByResponse para verificar si el fragmento actual está en la lista cargada.

6. Para segmentos no cargados, file.resume() continuará activando una solicitud POST para cargar el segmento.

7. La solicitud POST contendrá información como los datos y el desplazamiento de un fragmento.

8. El servidor recibe los datos fragmentados, los escribe en la ubicación especificada del archivo y devuelve una respuesta exitosa.

9. El componente de carga registrará que el segmento ha sido cargado.

10. Después de cargar todos los fragmentos en secuencia, el lado del servidor fusiona todos los fragmentos en un archivo completo.

11. Se llama a onFileSuccess para notificar que la carga se realizó correctamente.

12. De esta manera, a través de la solicitud GET para consultar los fragmentos cargados + POST carga de fragmentos inacabados + verificación, se realiza la reanudación del punto de interrupción / carga de fragmentos.

Aquí está el gráfico de carriles que dibujé:

6. Construya el proyecto SpringBoot

A continuación explicaré paso a paso cómo construir el backend de este proyecto, aquí una captura de pantalla del proyecto completo.

6.1 Preparativos 

6.1.1 Importar dependencias de pom

Esta es la información de dependencia completa del backend.

Código completo:

<dependencies>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- 常用工具类 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>

        <!-- MySQL依赖 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.29</version>
        </dependency>

        <!-- Mybatis依赖 -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.2</version>
        </dependency>

        <!-- Lombok依赖 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        
    </dependencies>

6.1.2, archivo de configuración yml

El puerto backend de configuración principal es 9090, el límite de archivo único es de hasta 100 MB, la información de configuración de MySQL y la información de configuración de MyBatis.

Código completo:

server:
  port: 9090

spring:
  servlet:
    multipart:
      max-request-size: 100MB
      max-file-size: 100MB
  datasource:
    username: 用户名
    password: 密码
    url: jdbc:mysql://localhost:3306/数据库名?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
    driver-class-name: com.mysql.cj.jdbc.Driver

# MyBatis配置
mybatis:
  # 搜索指定包别名
  typeAliasesPackage: com.example.**.domain
  # 配置mapper的扫描,找到所有的mapper.xml映射文件
  mapperLocations: classpath:mapping/*.xml
  # 加载全局的配置文件
  configLocation: classpath:mybatis/mybatis-config.xml

6.1.3, archivo de configuración mybatis-config.xml

Cree una nueva carpeta mybatis en la carpeta de recursos para almacenar el archivo de configuración de mybatis.

Código completo:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <!-- 全局参数 -->
    <settings>
        <!-- 使全局的映射器启用或禁用缓存 -->
        <setting name="cacheEnabled"             value="true"   />
        <!-- 允许JDBC 支持自动生成主键 -->
        <setting name="useGeneratedKeys"         value="true"   />
        <!-- 配置默认的执行器.SIMPLE就是普通执行器;REUSE执行器会重用预处理语句(prepared statements);BATCH执行器将重用语句并执行批量更新 -->
        <setting name="defaultExecutorType"      value="SIMPLE" />
        <!-- 指定 MyBatis 所用日志的具体实现 -->
        <setting name="logImpl"                  value="SLF4J"  />
        <!-- 使用驼峰命名法转换字段 -->
         <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings>

</configuration>

6.1.4, archivo SQL

Almacenar tabla de fragmentación de archivos:

CREATE TABLE `file_chunk`  (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
  `identifier` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '文件md5',
  `chunk_number` int NULL DEFAULT NULL COMMENT '当前分块序号',
  `chunk_size` bigint NULL DEFAULT NULL COMMENT '分块大小',
  `current_chunk_size` bigint NULL DEFAULT NULL COMMENT '当前分块大小',
  `total_size` bigint NULL DEFAULT NULL COMMENT '文件总大小',
  `total_chunks` int NULL DEFAULT NULL COMMENT '分块总数',
  `filename` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '文件名',
  `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '文件分片表' ROW_FORMAT = DYNAMIC;

Almacenar tabla de información del archivo:


CREATE TABLE `file_info`  (
  `id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '主键',
  `origin_file_name` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '文件源名',
  `file_name` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '新存放文件名',
  `file_path` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '文件存放路径',
  `file_size` bigint NULL DEFAULT NULL COMMENT '文件总大小',
  `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '文件信息表' ROW_FORMAT = Dynamic;

6.2, clase constante 

6.2.1, constantes de código de estado de retorno común de HttpStatus

Código completo:

package com.example.bigupload.constant;

/**
 * 返回状态码
 * @author HTT
 */
public class HttpStatus
{
    /**
     * 操作成功
     */
    public static final int SUCCESS = 200;

    /**
     * 对象创建成功
     */
    public static final int CREATED = 201;

    /**
     * 请求已经被接受
     */
    public static final int ACCEPTED = 202;

    /**
     * 操作已经执行成功,但是没有返回数据
     */
    public static final int NO_CONTENT = 204;

    /**
     * 资源已被移除
     */
    public static final int MOVED_PERM = 301;

    /**
     * 重定向
     */
    public static final int SEE_OTHER = 303;

    /**
     * 资源没有被修改
     */
    public static final int NOT_MODIFIED = 304;

    /**
     * 参数列表错误(缺少,格式不匹配)
     */
    public static final int BAD_REQUEST = 400;

    /**
     * 未授权
     */
    public static final int UNAUTHORIZED = 401;

    /**
     * 访问受限,授权过期
     */
    public static final int FORBIDDEN = 403;

    /**
     * 资源,服务未找到
     */
    public static final int NOT_FOUND = 404;

    /**
     * 不允许的http方法
     */
    public static final int BAD_METHOD = 405;

    /**
     * 资源冲突,或者资源被锁
     */
    public static final int CONFLICT = 409;

    /**
     * 不支持的数据,媒体类型
     */
    public static final int UNSUPPORTED_TYPE = 415;

    /**
     * 系统内部错误
     */
    public static final int ERROR = 500;

    /**
     * 接口未实现
     */
    public static final int NOT_IMPLEMENTED = 501;
}

6.3, clase de entidad

6.3.1, encapsulación de resultados unificada AjaxResult

Código completo:

package com.example.bigupload.domain;

import com.example.bigupload.constant.HttpStatus;
import org.apache.commons.lang3.ObjectUtils;

import java.util.HashMap;

/**
 * 操作消息提醒
 *
 * @author HTT
 */
public class AjaxResult extends HashMap<String, Object>
{
    private static final long serialVersionUID = 1L;

    /** 状态码 */
    public static final String CODE_TAG = "code";

    /** 返回内容 */
    public static final String MSG_TAG = "msg";

    /** 数据对象 */
    public static final String DATA_TAG = "data";

    /**
     * 初始化一个新创建的 AjaxResult 对象,使其表示一个空消息。
     */
    public AjaxResult()
    {
    }

    /**
     * 初始化一个新创建的 AjaxResult 对象
     *
     * @param code 状态码
     * @param msg 返回内容
     */
    public AjaxResult(int code, String msg)
    {
        super.put(CODE_TAG, code);
        super.put(MSG_TAG, msg);
    }

    /**
     * 初始化一个新创建的 AjaxResult 对象
     *
     * @param code 状态码
     * @param msg 返回内容
     * @param data 数据对象
     */
    public AjaxResult(int code, String msg, Object data)
    {
        super.put(CODE_TAG, code);
        super.put(MSG_TAG, msg);
        if (ObjectUtils.isNotEmpty(data))
        {
            super.put(DATA_TAG, data);
        }
    }

    /**
     * 返回成功消息
     *
     * @return 成功消息
     */
    public static AjaxResult success()
    {
        return AjaxResult.success("操作成功");
    }

    /**
     * 返回成功数据
     *
     * @return 成功消息
     */
    public static AjaxResult success(Object data)
    {
        return AjaxResult.success("操作成功", data);
    }

    /**
     * 返回成功消息
     *
     * @param msg 返回内容
     * @return 成功消息
     */
    public static AjaxResult success(String msg)
    {
        return AjaxResult.success(msg, null);
    }

    /**
     * 返回成功消息
     *
     * @param msg 返回内容
     * @param data 数据对象
     * @return 成功消息
     */
    public static AjaxResult success(String msg, Object data)
    {
        return new AjaxResult(HttpStatus.SUCCESS, msg, data);
    }

    /**
     * 返回错误消息
     *
     * @return
     */
    public static AjaxResult error()
    {
        return AjaxResult.error("操作失败");
    }

    /**
     * 返回错误消息
     *
     * @param msg 返回内容
     * @return 警告消息
     */
    public static AjaxResult error(String msg)
    {
        return AjaxResult.error(msg, null);
    }

    /**
     * 返回错误消息
     *
     * @param msg 返回内容
     * @param data 数据对象
     * @return 警告消息
     */
    public static AjaxResult error(String msg, Object data)
    {
        return new AjaxResult(HttpStatus.ERROR, msg, data);
    }

    /**
     * 返回错误消息
     *
     * @param code 状态码
     * @param msg 返回内容
     * @return 警告消息
     */
    public static AjaxResult error(int code, String msg)
    {
        return new AjaxResult(code, msg, null);
    }

    /**
     * 方便链式调用
     *
     * @param key 键
     * @param value 值
     * @return 数据对象
     */
    @Override
    public AjaxResult put(String key, Object value)
    {
        super.put(key, value);
        return this;
    }
}

6.3.2, clase de segmento de archivo

Código completo:

package com.example.bigupload.domain;

import lombok.Data;
import org.springframework.web.multipart.MultipartFile;

import java.io.Serializable;
import java.util.Date;

@Data
public class FileChunk implements Serializable {

    /**
     * 主键
     */
    private Long id;
    /**
     * 文件 md5
     */
    private String identifier;
    /**
     * 当前分块序号
     */
    private Integer chunkNumber;
    /**
     * 分块大小
     */
    private Long chunkSize;
    /**
     * 当前分块大小
     */
    private Long currentChunkSize;
    /**
     * 文件总大小
     */
    private Long totalSize;
    /**
     * 分块总数
     */
    private Integer totalChunks;
    /**
     * 文件名
     */
    private String filename;

    /**
     * 创建时间
     */
    private Date createTime;

    /**
     * 文件分片数据
     */
    private MultipartFile file;

}

6.3.3, clase de información de archivo

Código completo:

package com.example.bigupload.domain;

import lombok.Data;

import java.util.Date;

@Data
public class FileInfo {

    /**
     * 主键
     */
    private String id;

    /**
     * 文件源名
     */
    private String originFileName;

    /**
     * 新存放文件名
     */
    private String fileName;

    /**
     * 文件存放路径
     */
    private String filePath;

    /**
     * 文件总大小
     */
    private Long fileSize;

    /**
     * 创建时间
     */
    private Date createTime;

}

6.4, configuración global CORS entre dominios GlobalCorsConfig

1. La anotación @Configuration indica que se trata de una clase de configuración.

2. La interfaz WebMvcConfigurer se utiliza para personalizar la configuración de SpringMVC.

3. El método addCorsMappings se utiliza para definir políticas de acceso entre dominios.

4. addMapping("/**") significa interceptar todas las rutas de solicitud.

5. AllowOriginPatterns("*") indica que todas las solicitudes de nombres de dominio están permitidas.

6. enableCredentials(true) significa que se permiten cookies.

7. AllowHeaders indica los encabezados de solicitud permitidos.

8. AllowMethods indica los métodos de solicitud permitidos.

9. maxAge(3600) indica que el período de validez de la solicitud de verificación previa es de 3600 segundos.

De esta manera, se realiza una configuración global CORS entre dominios, lo que permite que las solicitudes de todos los nombres de dominio accedan a este servicio, y el encabezado y el método de la solicitud se pueden configurar libremente y el período de validez máximo es de 1 hora.

Código completo:

package com.example.bigupload.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class GlobalCorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOriginPatterns("*")
                .allowCredentials(true)
                .allowedHeaders("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedHeaders("Authorization", "Cache-Control", "Content-Type")
                .maxAge(3600);
    }
}

6.5, capa de persistencia

6.5.1, archivo FileChunkMapper.xml

Escriba principalmente dos SQL, uno para consultar toda la información de fragmentación de la base de datos de acuerdo con la información cifrada md5 y el otro para registrar la información del archivo de cada carga de fragmento exitosa.

Código completo:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.bigupload.mapper.FileChunkMapper">
    <select id="findFileChunkParamByMd5" resultType="com.example.bigupload.domain.FileChunk">
        SELECT * FROM file_chunk where identifier =#{identifier}
    </select>

    <select id="findCountByMd5" resultType="Integer">
        SELECT COUNT(*) FROM file_chunk where identifier =#{identifier}
    </select>

    <insert id="insertFileChunk" parameterType="com.example.bigupload.domain.FileChunk">
        INSERT INTO file_chunk
        <trim prefix="(" suffix=")" suffixOverrides=",">
            <if test="identifier != null">identifier,</if>
            <if test="chunkNumber != null">chunk_number,</if>
            <if test="chunkSize != null">chunk_size,</if>
            <if test="currentChunkSize != null">current_chunk_size,</if>
            <if test="totalSize != null">total_size,</if>
            <if test="totalChunks != null">total_chunks,</if>
            <if test="filename != null">filename,</if>
            <if test="createTime != null">create_time,</if>
        </trim>
        <trim prefix="VALUES (" suffix=")" suffixOverrides=",">
            <if test="identifier != null">#{identifier},</if>
            <if test="chunkNumber != null">#{chunkNumber},</if>
            <if test="chunkSize != null">#{chunkSize},</if>
            <if test="currentChunkSize != null">#{currentChunkSize},</if>
            <if test="totalSize != null">#{totalSize},</if>
            <if test="totalChunks != null">#{totalChunks},</if>
            <if test="filename != null">#{filename},</if>
            <if test="createTime != null">#{createTime},</if>
        </trim>
    </insert>
</mapper>

6.5.2, archivo FileChunkMapper

Código completo:

package com.example.bigupload.mapper;

import com.example.bigupload.domain.FileChunk;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

/**
 * @author HTT
 */
@Mapper
public interface FileChunkMapper {

    public List<FileChunk> findFileChunkParamByMd5(String identifier);

    public Integer findCountByMd5(String identifier);

    public int insertFileChunk(FileChunk fileChunk);
}

6.5.3, archivo FileInfoMapper.xml

Código completo:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.bigupload.mapper.FileInfoMapper">
    <insert id="insert">
        INSERT INTO file_info
            (id, origin_file_name, file_name, file_path, file_size, create_time)
        VALUES
            (#{id}, #{originFileName}, #{fileName}, #{filePath}, #{fileSize}, #{createTime});
    </insert>
</mapper>

6.5.4, archivo FileInfoMapper

package com.example.bigupload.mapper;

import com.example.bigupload.domain.FileInfo;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface FileInfoMapper {

  int insert(FileInfo fileInfo);

}

6.6 Clase de servicio principal de carga de archivos (importante)

6.6.1, la lógica de la verificación de carga de archivos

1. Según el identificador del archivo (md5), consulte si el archivo ya existe en la base de datos.

List<FileChunk> list = fileChunkMapper.findFileChunkParamByMd5(fileChunk.getIdentifier());
Map<String, Object> data = new HashMap<>(1);

2. Si no se puede encontrar, significa que el archivo no se ha cargado y se devuelve uploaded=false.

if (list == null || list.size() == 0) {
    data.put("uploaded", false);
    return AjaxResult.success("文件上传成功",data);
}

3. Si solo se encuentra una parte en la consulta, significa que se ha cargado el archivo completo y se devuelve uploaded=true.

if (list.get(0).getTotalChunks() == 1) {
    data.put("uploaded", true);
    data.put("url", "");
    return AjaxResult.success("文件上传成功",data);
}

4. Si se consultan varios datos, significa que se trata de un archivo grande cargado en pedazos.

5. Recorra los datos fragmentados, obtenga el número de cada bloque de archivos y guárdelo en la matriz uploadedFiles.

6. Finalmente, devuelva los fragmentos cargados al front-end.

// 处理分片
int[] uploadedFiles = new int[list.size()];
int index = 0;
for (FileChunk fileChunkItem : list) {
    uploadedFiles[index] = fileChunkItem.getChunkNumber();
    index++;
}
data.put("uploadedChunks", uploadedFiles);
return AjaxResult.success("文件上传成功",data);

7. Una vez que el front-end obtiene los datos, sabe qué fragmentos se han cargado en el archivo grande y qué fragmentos quedan por cargar.

8. Luego continúe cargando los fragmentos restantes en el punto de interrupción para lograr el efecto de reanudar en el punto de interrupción, por lo que este código verifica principalmente la base de datos del archivo para determinar si todo o parte del archivo ya existe, a fin de determinar si es necesario. para cargar el archivo completo o continuar con la carga del punto de interrupción.

De esta manera, se pueden evitar cargas repetidas y se puede mejorar la eficiencia de la transmisión, que es la lógica clave para lograr una transmisión instantánea y una transmisión reanudable.

clave:

    public AjaxResult checkUpload(FileChunk fileChunk) {
        List<FileChunk> list = fileChunkMapper.findFileChunkParamByMd5(fileChunk.getIdentifier());
        Map<String, Object> data = new HashMap<>(1);
        // 判断文件存不存在
        if (list == null || list.size() == 0) {
            data.put("uploaded", false);
            return AjaxResult.success("文件上传成功",data);
        }
        // 处理单文件
        if (list.get(0).getTotalChunks() == 1) {
            data.put("uploaded", true);
            data.put("url", "");
            return AjaxResult.success("文件上传成功",data);
        }
        // 处理分片
        int[] uploadedFiles = new int[list.size()];
        int index = 0;
        for (FileChunk fileChunkItem : list) {
            uploadedFiles[index] = fileChunkItem.getChunkNumber();
            index++;
        }
        data.put("uploadedChunks", uploadedFiles);
        return AjaxResult.success("文件上传成功",data);
    }

6.6.2 Implementar la lógica central de la carga de fragmentos

Este código utiliza RandomAccessFile para implementar la lógica de carga y escritura de fragmentos.

Lógica específica:

1. Cree un objeto RandomAccessFile, guárdelo de acuerdo con la ruta del archivo, abra el archivo en modo de lectura y escritura y utilícelo para realizar lectura y escritura de acceso aleatorio en el archivo.

RandomAccessFile randomAccessFile = new RandomAccessFile(filePath, "rw");

2. Calcule el tamaño del fragmento de cada fragmento. Si el front-end no lo pasa, use el valor predeterminado (50 MB, que es una constante que definí).

public static final long DEFAULT_CHUNK_SIZE = 50 * 1024 * 1024;
long chunkSize = fileChunk.getChunkSize() == 0L ? DEFAULT_CHUNK_SIZE : fileChunk.getChunkSize().longValue();

3. Calcule el desplazamiento del fragmento actual, que se calcula mediante (número de fragmento - 1) * tamaño del fragmento.

long offset = chunkSize * (fileChunk.getChunkNumber() - 1);

4. Utilice el método seek() para localizar la posición de desplazamiento del fragmento.

randomAccessFile.seek(offset);

5. Utilice el método write() para escribir los bytes del contenido del archivo del fragmento actual.

randomAccessFile.write(fileChunk.getFile().getBytes());

6. Repita este proceso hasta que se escriban todos los fragmentos.

7. Finalmente cierre el archivo.

randomAccessFile.close();

RandomAccessFile puede leer y escribir archivos en cualquier posición, por lo que puede escribir en la posición especificada en el orden de los fragmentos para lograr el efecto de carga de fragmentos. Este método puede aprovechar al máximo el caché de archivos del sistema operativo, que es más eficiente. Cada fragmento solo se escribe una vez y no es necesario leer ni modificar archivos, lo que ahorra operaciones de IO. Esta es la forma común de implementar la carga de fragmentos a través de RandomAccessFile, uniendo cada fragmento en un archivo completo.

Permítanme explicarlo de una manera fácil de entender presentando un ejemplo práctico sencillo:

Permítanme presentarles primero qué es el desplazamiento: se utiliza para determinar la posición de cada fragmento en todo el archivo.

Según este código clave:

// 计算每个分片大小 
long chunkSize = fileChunk.getChunkSize() == 0L ? DEFAULT_CHUNK_SIZE : fileChunk.getChunkSize().longValue();

// 计算分片的偏移量
long offset = chunkSize * (fileChunk.getChunkNumber() - 1); 

Desplazamiento = tamaño del fragmento * (número de secuencia del fragmento - 1)

Tome un archivo de 230 MB como ejemplo:

1. El tamaño del fragmento es de 50 MB.

2. El primer número de fragmento fileChunk, getChunkNumber() es 1.

3. Entonces el desplazamiento del primer segmento = 50 MB * (1 - 1) = 0.

El número de fragmento del segundo segmento es 2, luego el desplazamiento del segundo segmento = 50 MB * (2 - 1) = 50 MB. Por analogía, se puede calcular el desplazamiento de cada segmento.

La fórmula de desplazamiento del enésimo segmento es: desplazamiento = tamaño del segmento * (número de segmento del enésimo segmento - 1).

Entonces el proceso de ejecución completo es así:

El archivo de 230 MB se dividirá en 5 fragmentos:

1. El primer fragmento: 50 MB, el desplazamiento es 0.

2. El segundo fragmento: 50 MB, con un desplazamiento de 50 MB.

3. El tercer fragmento: 50 MB, con un desplazamiento de 100 MB.

4. El cuarto fragmento: 50 MB, con un desplazamiento de 150 MB.

5. El quinto fragmento: 30 MB, con un desplazamiento de 200 MB.

El cliente puede cargar estos 5 fragmentos simultáneamente:

1. Cargue el primer fragmento y escriba en el desplazamiento 0.

2. Cargue el segundo fragmento y escriba para compensar 50 MB.

3. Cargue el tercer fragmento y escriba en el desplazamiento de 100 MB.

4. Cargue el cuarto fragmento y escríbalo con un desplazamiento de 150 MB.

5. Cargue el quinto fragmento y escriba en el desplazamiento de 200 MB.

El lado del servidor puede escribir directamente el contenido de cada fragmento de acuerdo con el desplazamiento a través de RandomAccessFile. De esta manera, un archivo grande de 230 MB se puede cargar rápidamente a través de cinco fragmentos de 50 MB, logrando el efecto de la carga de fragmentos y mejorando la velocidad de transmisión.

clave:

    private void uploadFileByRandomAccessFile(String filePath, FileChunk fileChunk) throws IOException {

        RandomAccessFile randomAccessFile = new RandomAccessFile(filePath, "rw");
        // 分片大小必须和前端匹配,否则上传会导致文件损坏
        long chunkSize = fileChunk.getChunkSize() == 0L ? DEFAULT_CHUNK_SIZE : fileChunk.getChunkSize().longValue();
        // 偏移量
        long offset = chunkSize * (fileChunk.getChunkNumber() - 1);
        // 定位到该分片的偏移量
        randomAccessFile.seek(offset);
        // 写入
        randomAccessFile.write(fileChunk.getFile().getBytes());
        randomAccessFile.close();

    }

6.6.3 Procesamiento de fragmentación de archivos

Lo más importante aquí es el método uploadFileByRandomAccessFile, que implementa la lectura y escritura aleatoria de fragmentos de archivos y puede escribir los datos de los fragmentos de archivos cargados en la posición de desplazamiento correcta. 

Consulte el número total de fragmentos en la base de datos de acuerdo con el identificador único (MD5) del archivo. Si es igual al número total de fragmentos de front-end, significa que se ha cargado el archivo grande y la información completa del archivo. cargado esta vez se almacenará en la base de datos.

clave:

public AjaxResult uploadChunkFile(FileChunk fileChunk) throws IOException {

        String newFileName = fileChunk.getIdentifier() + fileChunk.getFilename();
        String filePath = "D:\\大文件分片存放\\" + newFileName;
        uploadFileByRandomAccessFile(filePath, fileChunk);
        fileChunk.setCreateTime(new Date());
        fileChunkMapper.insertFileChunk(fileChunk);

        //数据库中已上传的分片总数
        Integer count = fileChunkMapper.findCountByMd5(fileChunk.getIdentifier());
        if(fileChunk.getTotalChunks().equals(count)){
            FileInfo fileInfo = new FileInfo();
            String originalFilename = fileChunk.getFile().getOriginalFilename();
            fileInfo.setId(UUID.randomUUID().toString());
            fileInfo.setOriginFileName(originalFilename);
            fileInfo.setFileName(newFileName);
            fileInfo.setFilePath(filePath);
            fileInfo.setFileSize(fileChunk.getTotalSize());
            fileInfo.setCreateTime(new Date());
            fileInfoMapper.insert(fileInfo);
        }
        return AjaxResult.success("文件上传成功");
    }

6.6.4, código completo

package com.example.bigupload.service;

import com.example.bigupload.domain.AjaxResult;
import com.example.bigupload.domain.FileChunk;
import com.example.bigupload.domain.FileInfo;
import com.example.bigupload.mapper.FileChunkMapper;
import com.example.bigupload.mapper.FileInfoMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.*;

/**
 * @author HTT
 */
@Slf4j
@Service
public class UploadService {

    /**
     * 默认的分片大小:50MB
     */
    public static final long DEFAULT_CHUNK_SIZE = 50 * 1024 * 1024;

    @Resource
    private FileChunkMapper fileChunkMapper;

    @Resource
    private FileInfoMapper fileInfoMapper;

    public AjaxResult checkUpload(FileChunk fileChunk) {
        List<FileChunk> list = fileChunkMapper.findFileChunkParamByMd5(fileChunk.getIdentifier());
        Map<String, Object> data = new HashMap<>(1);
        // 判断文件存不存在
        if (list == null || list.size() == 0) {
            data.put("uploaded", false);
            return AjaxResult.success("文件上传成功",data);
        }
        // 处理单文件
        if (list.get(0).getTotalChunks() == 1) {
            data.put("uploaded", true);
            data.put("url", "");
            return AjaxResult.success("文件上传成功",data);
        }
        // 处理分片
        int[] uploadedFiles = new int[list.size()];
        int index = 0;
        for (FileChunk fileChunkItem : list) {
            uploadedFiles[index] = fileChunkItem.getChunkNumber();
            index++;
        }
        data.put("uploadedChunks", uploadedFiles);
        return AjaxResult.success("文件上传成功",data);
    }

    /**
     * 上传分片文件
     * @param fileChunk
     * @return
     * @throws Exception
     */
    public AjaxResult uploadChunkFile(FileChunk fileChunk) throws IOException {

        String newFileName = fileChunk.getIdentifier() + fileChunk.getFilename();
        String filePath = "D:\\大文件分片存放\\" + newFileName;
        uploadFileByRandomAccessFile(filePath, fileChunk);
        fileChunk.setCreateTime(new Date());
        fileChunkMapper.insertFileChunk(fileChunk);

        //数据库中已上传的分片总数
        Integer count = fileChunkMapper.findCountByMd5(fileChunk.getIdentifier());
        if(fileChunk.getTotalChunks().equals(count)){
            FileInfo fileInfo = new FileInfo();
            String originalFilename = fileChunk.getFile().getOriginalFilename();
            fileInfo.setId(UUID.randomUUID().toString());
            fileInfo.setOriginFileName(originalFilename);
            fileInfo.setFileName(newFileName);
            fileInfo.setFilePath(filePath);
            fileInfo.setFileSize(fileChunk.getTotalSize());
            fileInfo.setCreateTime(new Date());
            fileInfoMapper.insert(fileInfo);
        }
        return AjaxResult.success("文件上传成功");
    }

    private void uploadFileByRandomAccessFile(String filePath, FileChunk fileChunk) throws IOException {

        RandomAccessFile randomAccessFile = new RandomAccessFile(filePath, "rw");
        // 分片大小必须和前端匹配,否则上传会导致文件损坏
        long chunkSize = fileChunk.getChunkSize() == 0L ? DEFAULT_CHUNK_SIZE : fileChunk.getChunkSize().longValue();
        // 偏移量
        long offset = chunkSize * (fileChunk.getChunkNumber() - 1);
        // 定位到该分片的偏移量
        randomAccessFile.seek(offset);
        // 写入
        randomAccessFile.write(fileChunk.getFile().getBytes());
        randomAccessFile.close();

    }


}

6.7, capa de solicitud FileController

La escritura es muy concisa, la razón por la que todos son la misma interfaz /upload, el método de solicitud es get y post respectivamente, porque el componente de carga primero llama a la solicitud get de /upload al verificar, y luego carga el fragmento y llama al publicar solicitud de /upload .

1. Primero utilice la solicitud GET para verificar si el archivo existe y decida si implementar la transmisión instantánea, la transmisión de reanudación del punto de interrupción o la carga de varias partes.

2. Luego, el cliente utiliza solicitudes POST para cargar cada fragmento en orden y el servidor escribe los archivos después de recibir los fragmentos.

Código completo:

package com.example.bigupload.controller;

import com.example.bigupload.domain.AjaxResult;
import com.example.bigupload.domain.FileChunk;
import com.example.bigupload.service.UploadService;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;

@RestController
@RequestMapping("/file")
public class FileController {

    @Resource
    private UploadService uploadService;

    @GetMapping("/upload")
    public AjaxResult checkUpload(FileChunk fileChunk){
        return uploadService.checkUpload(fileChunk);
    }

    @PostMapping("/upload")
    public AjaxResult uploadChunkFile(FileChunk fileChunk) throws Exception {
        return uploadService.uploadChunkFile(fileChunk);
    }

}

7. Cree un proyecto de Vue

Aquí estoy usando Vue2.0, esta es la captura de pantalla completa del proyecto.

7.1 Preparativos

1. Instalar el cargador

npm install --save vue-simple-uploader

2. Según spark-md5

npm install --save spark-md5

3. Introducir componentes en main.js

import uploader from 'vue-simple-uploader'
Vue.use(uploader)

7.2, página InicioVer

De hecho, la lógica principal aquí es primero dividir y generar el código de verificación MD5 de este archivo a través del método getFileMD5, y luego usar el archivo Toda la información del fragmento cargado del archivo actual, y luego usar la lógica en el método checkChunkUploadedByResponse para juzgar qué fragmentos aún deben cargarse.

7.2.1 Uso del componente de carga

Este componente utiliza la biblioteca de terceros vue-uploader, que es un componente de carga.

Explicación del parámetro:

1. ref: asigne al componente un nombre de referencia, aquí está el cargador.

2. opciones: opciones de carga, como dirección de carga, tipos de archivos aceptados, etc.

3. inicio automático: si se debe comenzar a cargar automáticamente, el valor predeterminado es verdadero, aquí se establece en falso.

4. fileStatusText: texto de solicitud de estado de carga personalizado.

5. @file-added: el gancho cuando se agrega el archivo.

6. @file-success: enlace cuando el archivo se carga correctamente.

7. @file-error: Enganche cuando falla la carga del archivo.

8. @file-progress: enlace para el progreso de la carga de archivos.

Contiene tres subcomponentes:

1. uploader-unsupport: si el navegador no lo admite, muestra este componente.

2. uploader-drop: arrastre y suelte el componente del área de carga para personalizar el contenido.

3. uploader-btn: componente del botón de carga.

4. uploader-files: un componente de lista que ha seleccionado archivos para cargar.

Por lo tanto, este componente implementa funciones completas de carga de archivos, como seleccionar archivos, arrastrar y soltar cargas, mostrar el progreso de la carga y devolver los resultados de la carga. Podemos configurar los parámetros de carga a través de opciones, procesar los resultados de la carga a través de varios enlaces y personalizar el estilo del área de carga para implementar un componente de carga completo.

Código completo:

<template>
  <div class="home">
    <uploader
        ref="uploader"
        :options="options"
        :autoStart="false"
        :file-status-text="fileStatusText"
        @file-added="onFileAdded"
        @file-success="onFileSuccess"
        @file-error="onFileError"
        @file-progress="onFileProgress"
        class="uploader-example"
    >
      <uploader-unsupport></uploader-unsupport>
      <uploader-drop>
        <p>将文件拖放到此处以上传</p>
        <uploader-btn>选择文件</uploader-btn>
      </uploader-drop>
      <uploader-files> </uploader-files>
    </uploader>
    <br />
  </div>
</template>

7.2.2 Inicializar datos

Primero introduzca la herramienta Md5 e inicialice el tamaño del fragmento de antemano.

import SparkMD5 from "spark-md5";
const CHUNK_SIZE = 50 * 1024 * 1024;

parámetro de opciones:

1. destino: la dirección de la interfaz cargada

2. testChunks: si se habilita la verificación de carga de varias partes. La carga parcial consiste en dividir el archivo en partes pequeñas y cargarlas al mismo tiempo, lo que puede mejorar la eficiencia de la carga de archivos grandes.

3. uploadMethod: método de carga, el valor predeterminado es POST.

4. ChunkSize: tamaño del fragmento, el valor predeterminado es 1 MB. Esto se puede ajustar de acuerdo con la situación real: si el fragmento es demasiado pequeño, aumentará el número de solicitudes y si el fragmento es demasiado grande, se retransmitirá mucho si falla la carga.

5. Cargas simultáneas: la cantidad de bloques de carga simultáneos, el valor predeterminado es 3, que se puede ajustar según sea necesario.

7.2.3, método checkChunkUploadedByResponse (importante)

La función de este método es verificar si el fragmento cargado actualmente se ha cargado correctamente.

Este método solo llamará una vez a la solicitud de obtención proporcionada por la interfaz de carga para obtener toda la información del fragmento de este archivo desde el backend. Tiene dos parámetros:

fragmento: información sobre el fragmento actual, incluidos datos como el desplazamiento.

mensaje: llame a toda la información de fragmentación devuelta por la solicitud de obtención proporcionada por la interfaz de carga.

Si este método devuelve verdadero, significa que el segmento actual se cargó correctamente, entonces no es necesario llamar al método de carga para enviar una solicitud de carga posterior.

Dentro de la función, el mensaje de contenido de respuesta devuelto por el servidor se analiza primero en un objeto JSON.

let messageObj = JSON.parse(message);

Luego saque el campo de datos como un objeto de datos.

let dataObj = messageObj.data;

Luego juzgue si el campo cargado está incluido en el objeto de datos. Si hay un valor de carga devuelto directamente, el campo cargado generalmente es una señal del servidor que indica si se completó la carga del fragmento.

if (dataObj.uploaded !== undefined) {
    return dataObj.uploaded;
}

Si la lista de piezas cargadas es: [1, 2], es decir, las partes 1 y 2 se han cargado correctamente y las partes 3 y 4 aún no se han cargado.

El bloque actual a verificar es el bloque 2, desplazamiento = 1.

Entonces el proceso de ejecución es:

1. El valor de dataObj.uploadedChunks es [1, 2].

2. El valor de chunk.offset sigue siendo 1.

3. Chunk.offset+1 calcula que el número de secuencia del bloque 2 es 2.

4. indexOf(2) busca el valor 2 en la matriz [1, 2].

5. El valor del índice devuelto es 1, es decir, encontrado.

Entonces, para la parte 2, devuelva verdadero, lo que indica que la parte se cargó correctamente.

return (dataObj.uploadedChunks || []).indexOf(chunk.offset + 1) >= 0;

El propósito de la devolución es verificar si el bloque actual se ha cargado correctamente mirando la lista de bloques cargados devueltos por el servidor; de lo contrario, continúe llamando a la solicitud de publicación proporcionada por la carga para cargar.

7.2.4, método parseTimeRemaining

Método para formatear el tiempo de carga restante. La hora original tendrá el formato xx días xx horas xx minutos xx segundos.

clave:

parseTimeRemaining: function (timeRemaining, parsedTimeRemaining) {
    return parsedTimeRemaining
     .replace(/\syears?/, "年")
     .replace(/\days?/, "天")
     .replace(/\shours?/, "小时")
     .replace(/\sminutes?/, "分钟")
     .replace(/\sseconds?/, "秒");
},

Por lo tanto, estas configuraciones son principalmente para cargar en partes y mejorar la eficiencia y la experiencia de cargar archivos grandes a través de mecanismos como fragmentación, concurrencia y verificación.

clave:

    options: {
        target: 'http://127.0.0.1:9090/file/upload',
        testChunks: true,
        uploadMethod: "post",
        chunkSize: CHUNK_SIZE,
        // 并发上传数,默认为 3
        simultaneousUploads: 3,
        checkChunkUploadedByResponse: (chunk, message) => {
          let messageObj = JSON.parse(message);
          let dataObj = messageObj.data;
          if (dataObj.uploaded !== undefined) {
            return dataObj.uploaded;
          }
          return (dataObj.uploadedChunks || []).indexOf(chunk.offset + 1) >= 0;
        },
        parseTimeRemaining: function (timeRemaining, parsedTimeRemaining) {
          return parsedTimeRemaining
              .replace(/\syears?/, "年")
              .replace(/\days?/, "天")
              .replace(/\shours?/, "小时")
              .replace(/\sminutes?/, "分钟")
              .replace(/\sseconds?/, "秒");
        },
      },

7.2.5 Otros parámetros

Función de parámetro:

1. fileStatus: Defina la descripción de texto del estado de carga, que se utilizará en el componente que muestra el estado de carga.

2. uploadFileList: la lista de archivos que se han cargado correctamente, que se puede utilizar para guardar la información de los archivos cargados correctamente.

clave:

fileStatus: {
  success: "上传成功",
  error: "上传错误",
  uploading: "正在上传",
  paused: "停止上传",
  waiting: "等待中",
},
uploadFileList: [],

7.2.6, método onFileAdded (importante)

Lógica específica:

1. Agregue el archivo agregado a uploadFileList.

2. Después de calcular MD5, llame a file.resume().

3. file.resume() construirá una solicitud GET internamente, incluida la información MD5 del archivo, y la enviará al servidor.

4. Después de recibir la solicitud, el servidor verifica si el archivo existe según MD5.

5. Si existe, devuelva la información del bloque cargado.

6. Una vez que el front-end recibe la respuesta, llama a la lógica checkChunkUploadedByResponse.

7. checkChunkUploadedByResponse verifica el estado de carga de cada fragmento de acuerdo con los datos de respuesta y juzga qué fragmentos se han cargado y cuáles no de acuerdo con la respuesta.

8. Cargue los fragmentos no cargados. Para los fragmentos no cargados, file.resume() continuará activando la solicitud (POST) para cargar el fragmento hasta que todos los fragmentos se verifiquen y carguen.

Entonces, el método file.resume() incluye no solo la lógica de verificación de checkChunkUploadedByResponse, sino también la lógica para activar la carga parcial real.

clave:

    onFileAdded(file) {
      this.uploadFileList.push(file);
      // 2. 计算文件 MD5 并请求后台判断是否已上传,是则取消上传
      this.getFileMD5(file, (md5) => {
        if (md5 != "") {
          // 修改文件唯一标识
          file.uniqueIdentifier = md5;
          // 恢复上传
          file.resume();
        }
      });
    },

7.2.7, método getFileMD5 (importante)

Calcule el valor MD5 del archivo antes de cargarlo y realice el cálculo de acumulación de MD5 mientras lo corta. La lógica general es la siguiente:

Primero inicialice el objeto SparkMD5 para guardar el valor acumulativo de MD5 y luego lea el contenido del archivo en bloques a través de FileReader. Cada vez que lea un segmento del tamaño de un fragmento, al leer cada segmento, agregue el contenido del segmento al objeto Spark y realice MD5. acumulación y luego repita el proceso hasta que se lean todos los cortes. Finalmente, llame a spark.end() para obtener el valor MD5 general calculado acumulativamente del archivo.

Lógica específica:

1. Cree el objeto SparkMD5 para calcular el valor MD5.

let spark = new SparkMD5.ArrayBuffer();

2. Cree un objeto FileReader fileReader para leer el contenido del archivo.

let fileReader = new FileReader();

3. Obtenga la implementación compatible del método de corte del objeto Archivo, responsable de obtener el método de corte del archivo.

let blobSlice =
          File.prototype.slice ||
          File.prototype.mozSlice ||
          File.prototype.webkitSlice;

4. Calcule el número total de fragmentos y el tamaño de cada fragmento es CHUNK_SIZE.

let currentChunk = 0;
let chunks = Math.ceil(file.size / CHUNK_SIZE);

5. Registre la hora de inicio startTime.

let startTime = new Date().getTime();

6. Pausa la carga del archivo file.pause().

file.pause();

7. La función loadNext se utiliza para cargar fragmentos de archivos.

Lógica específica:

El primer paso es calcular la posición inicial del fragmento actual, que es el número de serie del fragmento actual currentChunk multiplicado por el tamaño del fragmento CHUNK_SIZE.

El segundo paso es calcular la posición final del fragmento actual: si el tamaño inicial + del fragmento es mayor o igual que el tamaño total file.size, entonces el final es el tamaño total file.size; de ​​lo contrario, el final es el inicio + tamaño del fragmento CHUNK_SIZE .

El tercer paso es obtener un objeto Blob de segmento de un archivo mediante el método blobSlice, que intercepta la parte desde el principio hasta el final del archivo. De hecho, se trata de llamar al método de división, pasar el objeto y el rango del archivo y realizar la operación de división del archivo real.

Paso 4: llame al método readAsArrayBuffer de FileReader para leer el contenido del objeto del fragmento Blob.

Paso 5. readAsArrayBuffer activará el evento de carga de FileReader y el contenido del fragmento se podrá obtener y procesar más adelante en la devolución de llamada del evento.

Entonces, la función de loadNext es obtener un fragmento Blob del archivo especificado de acuerdo con el tamaño del fragmento y el número de serie, y luego usar FileReader para leer el contenido de ArrayBuffer de este fragmento.

clave:

    function loadNext() {
        const start = currentChunk * CHUNK_SIZE;
        const end =
            start + CHUNK_SIZE >= file.size ? file.size : start + CHUNK_SIZE;
        fileReader.readAsArrayBuffer(blobSlice.call(file.file, start, end));
      }

8. Este código es la función de devolución de llamada del evento de carga de FileReader, que se utiliza para procesar el cálculo de acumulación MD5 después de leer el contenido del fragmento.

Lógica específica:

El primer paso es agregar el contenido fragmentado leído de ArrayBuffer para generar spark.append (e.target.result) y realizar la acumulación de MD5.

El segundo paso es juzgar que si el currentChunk del número de fragmento es aún menor que el número total de fragmentos, significa que todavía hay fragmentos sin leer: currentChunk se incrementa automáticamente y se llama a loadNext () para cargar el siguiente fragmento.

El tercer paso, de lo contrario, significa que se han leído todos los fragmentos: calcule el valor MD5 final md5 mediante spark y end (), imprima y calcule, lo que lleva mucho tiempo y devuelva el valor md5 mediante devolución de llamada.

El cuarto paso, la función de devolución de llamada recibirá md5 desde el exterior, juzgará si el archivo está duplicado y determinará la lógica de seguimiento.

Entonces, la función de este código es leer recursivamente los fragmentos del archivo, acumular y calcular MD5 y finalmente devolver el resultado MD5, completando la verificación MD5 de todo el archivo. Al cargar en partes, se evitan los problemas de rendimiento causados ​​por cargar todo el archivo grande a la vez y se logra un cálculo MD5 eficiente.

clave:

      fileReader.onload = function (e) {
        spark.append(e.target.result);
        if (currentChunk < chunks) {
          currentChunk++;
          loadNext();
        } else {
          let md5 = spark.end();
          console.log(
              `MD5计算完毕:${md5},耗时:${new Date().getTime() - startTime} ms.`
          );
          callback(md5);
        }
      };
      fileReader.onerror = function () {
        this.$message.error("文件读取错误");
        file.cancel();
      };

7.2.8, código completo

<template>
  <div class="home">
    <uploader
        ref="uploader"
        :options="options"
        :autoStart="false"
        :file-status-text="fileStatusText"
        @file-added="onFileAdded"
        @file-success="onFileSuccess"
        @file-error="onFileError"
        @file-progress="onFileProgress"
        class="uploader-example"
    >
      <uploader-drop>
        <p>将文件拖放到此处以上传</p>
        <uploader-btn>选择文件</uploader-btn>
      </uploader-drop>
      <uploader-files> </uploader-files>
    </uploader>
    <br />
  </div>
</template>

<script>
import HelloWorld from '@/components/HelloWorld.vue'
import SparkMD5 from "spark-md5";
const CHUNK_SIZE = 50 * 1024 * 1024;
export default {
  name: 'HomeView',
  components: {
    HelloWorld
  },
  data() {
    return {
      options: {
        target: 'http://127.0.0.1:9090/file/upload',
        testChunks: true,
        uploadMethod: "post",
        chunkSize: CHUNK_SIZE,
        simultaneousUploads: 3,
        checkChunkUploadedByResponse: (chunk, message) => {
          let messageObj = JSON.parse(message);
          let dataObj = messageObj.data;
          if (dataObj.uploaded !== undefined) {
            return dataObj.uploaded;
          }
          return (dataObj.uploadedChunks || []).indexOf(chunk.offset + 1) >= 0;
        },
        parseTimeRemaining: function (timeRemaining, parsedTimeRemaining) {
          return parsedTimeRemaining
              .replace(/\syears?/, "年")
              .replace(/\days?/, "天")
              .replace(/\shours?/, "小时")
              .replace(/\sminutes?/, "分钟")
              .replace(/\sseconds?/, "秒");
        },
      },
      fileStatus: {
        success: "上传成功",
        error: "上传错误",
        uploading: "正在上传",
        paused: "停止上传",
        waiting: "等待中",
      },
      uploadFileList: [],
    };
  },
  methods: {
    onFileAdded(file) {
      this.uploadFileList.push(file);
      // 2. 计算文件 MD5 并请求后台判断是否已上传,是则取消上传
      this.getFileMD5(file, (md5) => {
        if (md5 != "") {
          // 修改文件唯一标识
          file.uniqueIdentifier = md5;
          // 恢复上传
          file.resume();
        }
      });
    },
    onFileSuccess(rootFile, file, response, chunk) {
      console.log("上传成功");
    },
    onFileError(rootFile, file, message, chunk) {
      console.log("上传出错:" + message);
    },
    onFileProgress(rootFile, file, chunk) {
      console.log(`当前进度:${Math.ceil(file._prevProgress * 100)}%`);
    },
    
    getFileMD5(file, callback) {
      let spark = new SparkMD5.ArrayBuffer();
      let fileReader = new FileReader();
      let blobSlice =
          File.prototype.slice ||
          File.prototype.mozSlice ||
          File.prototype.webkitSlice;
      let currentChunk = 0;
      let chunks = Math.ceil(file.size / CHUNK_SIZE);
      let startTime = new Date().getTime();
      file.pause();
      loadNext();
      fileReader.onload = function (e) {
        spark.append(e.target.result);
        if (currentChunk < chunks) {
          currentChunk++;
          loadNext();
        } else {
          let md5 = spark.end();
          console.log(
              `MD5计算完毕:${md5},耗时:${new Date().getTime() - startTime} ms.`
          );
          callback(md5);
        }
      };
      fileReader.onerror = function () {
        this.$message.error("文件读取错误");
        file.cancel();
      };

      function loadNext() {
        const start = currentChunk * CHUNK_SIZE;
        const end =
            start + CHUNK_SIZE >= file.size ? file.size : start + CHUNK_SIZE;
        fileReader.readAsArrayBuffer(blobSlice.call(file.file, start, end));
      }
    },
    fileStatusText(status) {
      console.log(11111)
      console.log(status)
      if (status === "md5") {
        return "校验MD5";
      }
      return this.fileStatus[status];
    },
  },
}
</script>

Ocho, ejecuta el proyecto.

A continuación, déjame ejecutar este proyecto y mostrarte el efecto.

8.1 Subir en pedazos

Preparé un archivo con un tamaño de 141,1 MB para cargarlo de antemano y lo corté en 2 partes de forma predeterminada para cargarlo.

Es la información escrita en la base de datos:

Información del fragmento:

Informacion del archivo:

¿Se pregunta por qué el archivo 141M no se corta en 3 partes, sino solo en 2 partes, y el tamaño de la segunda parte es de 91 MB? Permítame explicarle el motivo en detalle a continuación: 

Esto se debe a que, en teoría, el archivo de 141 M se dividirá en tres archivos fragmentados de 50 MB, 50 MB y 41 MB, pero los últimos 41 MB no alcanzan los 50 MB. Los 3 bloques se fusionan para reducir el número de divisiones, por lo que los cortes finales son de 50 MB. y 91 MB.

8.2 Currículum del punto de interrupción 

A veces, puede haber algunos motivos de red o el cliente quiere guardar el resto y subirlo mañana. 

La próxima vez, el progreso de la última carga se mantendrá hasta que la carga se realice correctamente. 

Esta es la información escrita en la base de datos:

Información del fragmento:

Porque solo se carga una parte de la información del fragmento, pero la base de datos la registrará. 

Informacion del archivo:

Debido a que el archivo no se ha cargado por completo, la tabla de información del archivo no almacenará la información del archivo. 

La próxima vez, continuaremos subiendo según el progreso de la última vez.

Esta es la información escrita en la base de datos: 

Información del fragmento:

Informacion del archivo:

8.3 Segunda pasada 

Es muy sencillo de cargar en segundos. Elijo cargar el archivo grande ahora mismo. 

Obviamente, no se realiza ningún corte, solo se llama una vez a la interfaz de verificación y toda la información del fragmento de este archivo se consulta desde la base de datos de acuerdo con MD5 y se devuelve al front-end. En comparación con la información del fragmento del front-end, es Verificó que el archivo se haya cargado por completo, por lo que la carga se completa en 1 segundo. 

9. Dirección del código fuente de Gitee

Aquí abro el código completo del proyecto, ¡para que puedas aprenderlo tú mismo!

Front-end: Vue implementa la transferencia de archivos grandes en segundos + transferencia de reanudación del punto de interrupción + carga de fragmentos

Back-end: SpringBoot implementa la transmisión de archivos grandes en segundos + transmisión de reanudación del punto de interrupción + carga de fragmentos

10. Resumen

Lo anterior es mi explicación personal del proceso completo de Vue + SpringBoot para realizar la transferencia de archivos grandes en segundos, la transferencia de reanudación del punto de interrupción y la carga de fragmentos. Casi todos los lugares están en su lugar. Si tiene alguna pregunta, bienvenido a discutir en el área de comentarios. !

Supongo que te gusta

Origin blog.csdn.net/HJW_233/article/details/132224072
Recomendado
Clasificación