[Texte long de mille mots] Vue+SpringBoot réalise une seconde transmission de fichiers volumineux, une reprise de la transmission du point d'arrêt et un didacticiel complet de téléchargement de fragments (code source Gitee fourni)

Avant-propos : J'ai récemment rencontré une exigence dans un projet réel. Les clients peuvent télécharger des fichiers relativement volumineux. Si la solution de téléchargement de fichiers traditionnelle est utilisée, il peut y avoir certains risques de sécurité tels qu'une pression élevée sur le serveur, un gaspillage de ressources et même un débordement de mémoire. Par conséquent, afin de résoudre une série de Le problème est qu'une nouvelle solution technique est nécessaire pour réaliser le téléchargement de fichiers volumineux, quand je suis libre, je me réfère à quelques tutoriels connexes sur Internet, et enfin je le résume moi-même. Dans ce blog, Je vais vous expliquer étape par étape comment je réalise des fichiers volumineux. Pour les trois fonctions de seconde transmission, de reprise de transmission au point d'arrêt et de fragmentation, chaque morceau de code sera expliqué. A la fin du blog, je fournirai le code source de Gitee pour tout le monde. Télécharger.

Table des matières

1. Pourquoi utiliser cette solution technique 

2. Qu'est-ce que la transmission instantanée

3. Qu'est-ce que la reprise à partir du point d'arrêt 

4. Qu'est-ce que le téléchargement partitionné

5. Processus de téléchargement

6. Construisez le projet SpringBoot

6.1. Préparatifs 

6.1.1. Importer les dépendances pom

6.1.2, fichier de configuration yml

6.1.3, fichier de configuration mybatis-config.xml

6.1.4, fichier SQL

6.2, classe constante 

6.2.1, Constantes du code d'état de retour commun HttpStatus

6.3, Classe d'entité

6.3.1, Encapsulation unifiée des résultats AjaxResult

6.3.2, classe de tranche de fichier

6.3.3, classe d'informations sur les fichiers

6.4, configuration interdomaine globale CORS de GlobalCorsConfig

6.5, couche de persistance

6.5.1, fichier FileChunkMapper.xml

6.5.2, fichier FileChunkMapper

6.5.3, fichier FileInfoMapper.xml

6.5.4, fichier FileInfoMapper

6.6. Classe de service principale de téléchargement de fichiers (important)

6.6.1, la logique de vérification du téléchargement de fichiers

6.6.2. Réaliser la logique de base du téléchargement de fragments

6.6.3. Traitement de fragmentation de fichiers

6.6.4, code complet

6.7, couche de requête FileController

7. Construire un projet Vue

7.1. Préparatifs

7.2, page AccueilView

7.2.1. Utilisation du composant Uploader

7.2.2. Initialiser les données

7.2.3, méthode checkChunkUploadedByResponse (important)

7.2.4, méthode parseTimeRemaining

7.2.5. Autres paramètres

7.2.6, méthode onFileAdded (important)

7.2.7, méthode getFileMD5 (important)

7.2.8, code complet

Huit, lancez le projet

8.1. Télécharger en morceaux

8.2. Reprise du point d'arrêt 

8.3. Deuxième passage 

9. Adresse du code source du Gitee

10. Résumé


1. Pourquoi utiliser cette solution technique 

Si le frontal télécharge un fichier très volumineux (tel que 1G) en une seule fois et n'adopte pas de solutions techniques telles que la transmission de reprise par fragmentation/point d'arrêt, il sera principalement confronté aux dangers ou problèmes cachés suivants :

1. La vitesse de transmission du réseau est lente

Le temps de téléchargement est long et le téléchargement complet du fichier doit occuper une bande passante en amont continue et stable. Si l'état du réseau n'est pas bon, le téléchargement sera très lent, ce qui nuira à l'expérience utilisateur.

2. S'il échoue au milieu, vous devez télécharger à nouveau

Si le processus de téléchargement est interrompu pour des raisons telles que le réseau, la transmission entière échouera. Cela nécessite simplement que l'utilisateur télécharge à nouveau le fichier complet, en répétant le travail.

3. Le serveur est soumis à une forte pression

Le serveur doit occuper beaucoup de ressources pour traiter en continu un fichier volumineux, ce qui exerce une forte pression sur les performances du serveur et peut affecter d'autres services.

4. Gaspillage des ressources de trafic

Si un fichier volumineux est téléchargé entièrement en une seule fois, si le même fichier existe déjà, il consommera à plusieurs reprises une grande quantité de trafic réseau, ce qui constitue un gaspillage de données.

5. Il est difficile de réaliser l'invite de progression du téléchargement

L'utilisateur ne peut pas percevoir la progression du téléchargement et ne sait pas quelle quantité de données a été téléchargée si le téléchargement échoue.

Par conséquent, afin de résoudre ces problèmes, il est très important d’utiliser des technologies telles que la fragmentation et la reprise de la transmission. Il peut télécharger des blocs de données par lots, évitant ainsi l'inconvénient de les télécharger en une seule fois. Dans le même temps, combiné à des méthodes telles que la vérification et l'enregistrement des fragments téléchargés, il peut rendre l'ensemble du processus de téléchargement contrôlable, récupérable, économiser le trafic et améliorer considérablement l'efficacité de la transmission.

2. Qu'est-ce que la transmission instantanée

J'utiliserai ce projet pour expliquer la logique de mise en œuvre de Miaochuan d'une manière facile à comprendre.

1. Lorsque le client Vue télécharge un fichier, il calcule d'abord la valeur MD5 du fichier, puis envoie la valeur MD5 au serveur SpringBoot.

2. Une fois que le serveur SpringBoot a reçu la valeur MD5 , il utilise MyBatis pour interroger la base de données MySQL afin de vérifier si un fichier avec la même valeur MD5 existe déjà .

3. S'il existe, cela signifie que le fichier a été téléchargé, et le serveur demande directement quels fragments existent dans le fichier à partir de la base de données et les renvoie au client.

4. Une fois que le client a obtenu les informations sur la fragmentation du fichier, il assemblera directement le fichier complet sans télécharger le contenu réel du fichier.

5. Si la valeur MD5 n'existe pas dans la base de données, cela signifie que le fichier n'a pas été téléchargé et le serveur indiquera que le client doit télécharger l'intégralité du fichier.

6. Une fois que le client a téléchargé le fichier, le serveur ajoutera la relation correspondante entre le fichier et MD 5 dans la base de données MySQL et stockera les informations de fragmentation du fichier.

7. Lorsque le même fichier sera téléchargé la prochaine fois, la valeur MD5 peut être utilisée pour le télécharger en quelques secondes.

L'essentiel est donc d'utiliser la base de données MySQL pour enregistrer les informations MD5 et de fragmentation de chaque fichier , d'interroger via MD5 lors du téléchargement , et MySQL peut juger s'il faut autoriser la transmission instantanée, afin d'éviter les téléchargements répétés du même fichier.

3. Qu'est-ce que la reprise à partir du point d'arrêt 

Ensuite, j'expliquerai le concept de reprise de la transmission avec ce projet d'une manière facile à comprendre.

1. Lors du téléchargement d'un fichier, Vue frontale coupe le fichier en plusieurs petits morceaux et télécharge un petit morceau à chaque fois.

2. Chaque fois qu'un petit bloc est téléchargé, le backend SpringBoot enregistrera les informations de ce petit bloc, telles que le numéro de série du petit bloc, le fichier MD5, le hachage du contenu, etc. Peut être enregistré dans une base de données MySQL.

3. Si le téléchargement est interrompu, le côté Vue peut demander à SpringBoot quels petits éléments ont été téléchargés.

4. SpringBoot interroge la base de données et renvoie la petite information téléchargée à Vue.

5. Vue peut continuer à télécharger uniquement la petite partie interrompue au milieu.

6. SpringBoot va réassembler ces petits blocs en un fichier complet selon le numéro de série des petits blocs et le MD5 du fichier.

7. Si le fichier est à nouveau téléchargé ultérieurement, la valeur MD5 peut être utilisée pour savoir que le fichier existe déjà, et le succès du téléchargement sera renvoyé directement sans télécharger le contenu réel. De cette manière, en téléchargeant des tranches et en enregistrant de manière persistante les informations des tranches téléchargées, il est possible de reprendre le téléchargement à partir des points d'arrêt.

La clé est que SpringBoot doit fournir une interface pour enregistrer et obtenir les informations de tranche téléchargées. Le côté Vue doit découper et télécharger dans l'ordre. Enfin, SpringBoot rassemble les fichiers. En termes simples, le téléchargement est interrompu en raison du réseau et d'autres raisons. En enregistrant la quantité de données transmises, en Une solution technique pour continuer à télécharger les données restantes après une interruption.

4. Qu'est-ce que le téléchargement partitionné

En fin de compte, j'expliquerai le concept de téléchargement en plusieurs parties d'une manière facile à comprendre sur la base de ce projet.

1. Le but du téléchargement en morceaux est de couper un fichier volumineux en plusieurs petits morceaux pour obtenir des téléchargements simultanés afin d'améliorer la vitesse de transmission.

2. Les fichiers volumineux peuvent être divisés en fonction de la taille du fragment configurée (par exemple, un fragment de 50 Mo).

3. Le projet Vue télécharge chaque fractionnement sur le serveur SpringBoot dans l'ordre, puis télécharge chaque élément dans l'ordre.

4. Une fois que le serveur SpringBoot a reçu le fragment, il peut le stocker temporairement localement, enregistrer les informations caractéristiques du fragment, telles que le numéro de série du fragment, le fichier MD5, etc., et les écrire dans la base de données.

5. Après avoir téléchargé tous les fragments, SpringBoot les réassemble dans l'ordre dans les fichiers originaux complets.

5. Processus de téléchargement

Voici la logique de traitement des tranches frontales que j'ai dessinée :

Voici la logique que j'ai dessinée pour le traitement des tranches dans le backend :

Voici ma synthèse logique du processus global d’exécution du projet : 

1. Une fois créé, initialisez le composant de téléchargement, spécifiez la taille du fragment, la méthode de téléchargement et d'autres configurations.

2. Dans la méthode onFileAdded, une fois le MD5 calculé pour le fichier sélectionné, appelez file.resume() pour lancer le téléchargement.

3. file.resume() envoie d'abord une requête GET en interne pour demander au serveur les fragments téléchargés du fichier.

4. Le serveur renvoie un JSON contenant une liste de fragments téléchargés.

5. Le composant de téléchargement appelle checkChunkUploadedByResponse pour vérifier si le morceau actuel figure dans la liste téléchargée.

6. Pour les segments non téléchargés, file.resume() continuera à déclencher une requête POST pour télécharger le segment.

7. La requête POST contiendra des informations telles que les données et le décalage d'un fragment.

8. Le serveur reçoit les données fragmentées, les écrit à l'emplacement spécifié du fichier et renvoie une réponse réussie.

9. Le composant de téléchargement enregistrera que le segment a été téléchargé.

10. Après avoir téléchargé tous les fragments dans l'ordre, le côté serveur fusionne tous les fragments dans un fichier complet.

11. onFileSuccess est appelé pour notifier que le téléchargement a réussi.

12. De cette manière, grâce à la requête GET pour se renseigner sur les fragments téléchargés + le téléchargement POST des fragments inachevés + la vérification, la reprise du point d'arrêt/le téléchargement du fragment est réalisé.

Voici le graphique à couloirs que j'ai dessiné :

6. Construisez le projet SpringBoot

Ensuite, j'expliquerai étape par étape comment construire le backend de ce projet. Voici une capture d'écran du projet complet.

6.1. Préparatifs 

6.1.1. Importer les dépendances pom

Il s'agit des informations complètes sur les dépendances du backend.

Code complet :

<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, fichier de configuration yml

Le port backend de configuration principal est 9090, la limite de fichier unique peut aller jusqu'à 100 Mo, les informations de configuration de MySQL et les informations de configuration de MyBatis.

Code complet :

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, fichier de configuration mybatis-config.xml

Créez un nouveau dossier mybatis sous le dossier de ressources pour stocker le fichier de configuration de mybatis.

Code complet :

<?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, fichier SQL

Table de fragmentation des fichiers du magasin :

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;

Tableau d'informations sur le fichier de stockage :


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, classe constante 

6.2.1, Constantes du code d'état de retour commun HttpStatus

Code complet :

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, Classe d'entité

6.3.1, Encapsulation unifiée des résultats AjaxResult

Code complet :

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, classe de tranche de fichier

Code complet :

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, classe d'informations sur les fichiers

Code complet :

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, configuration interdomaine globale CORS de GlobalCorsConfig

1. L'annotation @Configuration indique qu'il s'agit d'une classe de configuration.

2. L'interface WebMvcConfigurer est utilisée pour personnaliser la configuration de SpringMVC.

3. La méthode addCorsMappings est utilisée pour définir des politiques d'accès inter-domaines.

4. addMapping("/**") signifie intercepter tous les chemins de requête.

5. AllowOriginPatterns("*") indique que toutes les demandes de noms de domaine sont autorisées.

6. allowCredentials(true) signifie que les cookies sont autorisés.

7. AllowedHeaders indique les en-têtes de requête autorisés.

8. AllowMethods indique les méthodes de requête autorisées.

9. maxAge(3600) indique que la période de validité de la demande de contrôle en amont est de 3600 secondes.

De cette manière, une configuration globale inter-domaines CORS est réalisée, permettant aux requêtes de tous les noms de domaine d'accéder à ce service, et l'en-tête et la méthode de la requête peuvent être définis librement, et la période de validité maximale est de 1 heure.

Code complet :

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, couche de persistance

6.5.1, fichier FileChunkMapper.xml

Écrivez principalement deux SQL, l'un consiste à interroger toutes les informations de fragmentation de la base de données en fonction des informations cryptées md5 et l'autre consiste à enregistrer les informations de fichier de chaque téléchargement de fragment réussi.

Code complet :

<?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, fichier FileChunkMapper

Code complet :

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, fichier FileInfoMapper.xml

Code complet :

<?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, fichier 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. Classe de service principale de téléchargement de fichiers (important)

6.6.1, la logique de vérification du téléchargement de fichiers

1. Selon l'identifiant du fichier (md5), demandez si le fichier existe déjà dans la base de données.

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

2. S'il est introuvable, cela signifie que le fichier n'a pas été téléchargé et uploaded=false est renvoyé.

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

3. Si un seul élément est trouvé dans la requête, cela signifie que l'intégralité du fichier a été téléchargé et uploaded=true est renvoyé.

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

4. Si plusieurs éléments de données sont interrogés, cela signifie qu'il s'agit d'un fichier volumineux téléchargé en plusieurs morceaux.

5. Parcourez les données fragmentées, obtenez le numéro de chaque bloc de fichiers et enregistrez-le dans le tableau uploadedFiles.

6. Enfin, renvoyez les uploadedChunks au 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. Une fois que le frontal a obtenu les données, il sait quels fragments ont été téléchargés dans le fichier volumineux et quels fragments restent à télécharger.

8. Continuez ensuite à télécharger les fragments restants au point d'arrêt pour obtenir l'effet de reprise au point d'arrêt. Ainsi, ce code vérifie principalement la base de données du fichier pour déterminer si tout ou partie du fichier existe déjà, afin de déterminer s'il est nécessaire pour télécharger le fichier complet ou continuer le téléchargement du point d'arrêt.

De cette manière, les téléchargements répétés peuvent être évités et l'efficacité de la transmission peut être améliorée, ce qui constitue la logique clé pour réaliser une transmission instantanée et une transmission pouvant reprendre.

code clé:

    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. Réaliser la logique de base du téléchargement de fragments

Ce code consiste à utiliser RandomAccessFile pour réaliser la logique de téléchargement et d'écriture de fragments.

Logique spécifique :

1. Créez un objet RandomAccessFile, stockez-le en fonction du chemin du fichier, ouvrez le fichier en mode lecture et écriture et utilisez-le pour effectuer une lecture et une écriture à accès aléatoire dans le fichier.

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

2. Calculez la taille de morceau de chaque fragment. Si le frontal ne le transmet pas, utilisez la valeur par défaut (50 Mo, qui est une constante que j'ai définie).

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

3. Calculez le décalage du fragment actuel, qui est calculé par (numéro de fragment - 1) * taille du fragment.

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

4. Utilisez la méthode seek() pour localiser la position décalée du fragment.

randomAccessFile.seek(offset);

5. Utilisez la méthode write() pour écrire les octets du contenu du fichier du fragment actuel.

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

6. Répétez ce processus jusqu'à ce que toutes les partitions soient écrites.

7. Fermez enfin le fichier.

randomAccessFile.close();

RandomAccessFile peut lire et écrire des fichiers à n'importe quelle position, il peut donc écrire à la position spécifiée dans l'ordre des fragments pour obtenir l'effet de téléchargement de fragments. Cette méthode permet d'utiliser pleinement le cache de fichiers du système d'exploitation, ce qui est plus efficace. Chaque fragment n'est écrit qu'une seule fois et il n'est pas nécessaire de lire et de modifier les fichiers, ce qui enregistre les opérations d'E/S. Il s'agit de la manière courante d'implémenter le téléchargement de fragments via RandomAccessFile, en fusionnant chaque fragment en un fichier complet.

Permettez-moi de l'expliquer d'une manière facile à comprendre en introduisant un exemple pratique simple :

Permettez-moi d'abord de vous présenter ce qu'est le décalage : il est utilisé pour déterminer la position de chaque fragment dans l'ensemble du fichier.

D'après ce code clé :

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

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

Décalage = taille du fragment * (numéro de séquence du fragment - 1)

Prenons l'exemple d'un fichier de 230 Mo :

1. La taille du morceau est de 50 Mo.

2. Le premier numéro de fragment fileChunk, getChunkNumber() est 1.

3. Ensuite, le décalage de la première tranche = 50 Mo * (1 - 1) = 0.

Le numéro de fragment de la deuxième tranche est 2, alors le décalage de la deuxième tranche = 50 Mo * (2 - 1) = 50 Mo. Par analogie, le décalage de chaque tranche peut être calculé.

La formule de décalage de la nième tranche est : offset = taille de tranche * (numéro de segment de la nième tranche - 1).

Le processus d’exécution complet ressemble donc à ceci :

Le fichier de 230 Mo sera divisé en 5 fragments :

1. Le premier fragment : 50 Mo, le décalage est de 0.

2. Le deuxième fragment : 50 Mo, avec un décalage de 50 Mo.

3. Le troisième fragment : 50 Mo, avec un décalage de 100 Mo.

4. Le quatrième fragment : 50 Mo, avec un décalage de 150 Mo.

5. Le cinquième fragment : 30 Mo, avec un décalage de 200 Mo.

Le client peut télécharger ces 5 fragments simultanément :

1. Téléchargez le premier fragment et écrivez sur le décalage 0.

2. Téléchargez le deuxième fragment et écrivez sur un décalage de 50 Mo.

3. Téléchargez le troisième fragment et écrivez dans le décalage de 100 Mo.

4. Téléchargez le quatrième fragment et écrivez-le avec un décalage de 150 Mo.

5. Téléchargez le cinquième fragment et écrivez dans le décalage de 200 Mo.

Le côté serveur peut écrire directement le contenu de chaque fragment en fonction du décalage via RandomAccessFile. De cette façon, le gros fichier de 230 Mo peut être rapidement téléchargé via cinq fragments de 50 Mo, réalisant l'effet du téléchargement de fragments et améliorant la vitesse de transmission.

code clé:

    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. Traitement de fragmentation de fichiers

La chose la plus importante ici est la méthode uploadFileByRandomAccessFile, qui implémente la lecture et l'écriture aléatoires de fragments de fichiers et peut écrire les données des fragments de fichiers téléchargés dans la position de décalage correcte. 

Interrogez le nombre total de fragments dans la base de données en fonction de l'identifiant unique (MD5) du fichier. S'il est égal au nombre total de tranches frontales, cela signifie que le fichier volumineux a été téléchargé et que les informations complètes du fichier téléchargé cette fois sera stocké dans la base de données.

code clé:

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, code complet

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, couche de requête FileController

L'écriture est très concise, la raison pour laquelle ils sont tous identiques /upload, la méthode de requête est respectivement get et post, car le composant uploader appelle d'abord la requête get de /upload lors de la vérification, puis télécharge le fragment et appelle le poster la demande de /upload .

1. Utilisez d'abord la requête GET pour vérifier si le fichier existe et décidez s'il faut mettre en œuvre une transmission instantanée, une reprise de la transmission au point d'arrêt ou un téléchargement en plusieurs parties.

2. Ensuite, le client utilise les requêtes POST pour télécharger chaque fragment dans l'ordre, et le serveur écrit les fichiers après avoir reçu les fragments.

Code complet :

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. Construire un projet Vue

Ici j'utilise Vue2.0, voici la capture d'écran complète du projet

7.1. Préparatifs

1. Installer le téléchargeur

npm install --save vue-simple-uploader

2. Selon spark-md5

npm install --save spark-md5

3. Introduire les composants dans main.js

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

7.2, page AccueilView

En fait, la logique principale ici est d'abord de découper et de générer le code de vérification MD5 de ce fichier via la méthode getFileMD5, puis d'utiliser le fichier.Toutes les informations de fragment téléchargées du fichier actuel, puis d'utiliser la logique de la méthode checkChunkUploadedByResponse pour juger quels fragments doivent encore être téléchargés.

7.2.1. Utilisation du composant Uploader

Ce composant utilise la bibliothèque tierce vue-uploader, qui est un composant de téléchargement.

Explication des paramètres :

1. ref : donnez au composant un nom de référence, voici le téléchargeur.

2. options : options de téléchargement, telles que l'adresse de téléchargement, les types de fichiers acceptés, etc.

3. autoStart : s'il faut démarrer le téléchargement automatiquement, la valeur par défaut est true, ici elle est définie sur false.

4. fileStatusText : texte d'invite d'état de téléchargement personnalisé.

5. @file-added : Le hook lorsque le fichier est ajouté.

6. @file-success : hook lorsque le fichier est téléchargé avec succès.

7. @file-error : hook lorsque le téléchargement du fichier échoue.

8. @file-progress : Hook pour la progression du téléchargement des fichiers.

Contient trois sous-composants :

1. uploader-unsupport : Si le navigateur ne le prend pas en charge, affichez ce composant.

2. uploader-drop : faites glisser et déposez le composant de la zone de téléchargement pour personnaliser le contenu.

3. uploader-btn : composant du bouton de téléchargement.

4. uploader-files : un composant de liste qui a sélectionné les fichiers à télécharger.

Par conséquent, ce composant implémente des fonctions complètes de téléchargement de fichiers telles que la sélection de fichiers, le glisser-déposer des téléchargements, l'affichage de la progression du téléchargement et le rappel des résultats du téléchargement. Nous pouvons configurer les paramètres de téléchargement via des options, traiter les résultats du téléchargement via divers hooks et personnaliser le style de la zone de téléchargement pour implémenter un composant de téléchargement complet.

Code complet :

<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. Initialiser les données

Introduisez d’abord l’outil Md5 et initialisez la taille du fragment à l’avance.

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

paramètre d'options :

1. cible : l'adresse de l'interface téléchargée

2. testChunks : s'il faut activer la vérification du téléchargement en plusieurs parties. Le téléchargement partiel consiste à diviser le fichier en petits morceaux et à les télécharger simultanément, ce qui peut améliorer l'efficacité du téléchargement de fichiers volumineux.

3. uploadMethod : méthode de téléchargement, la valeur par défaut est POST.

4. chunkSize : taille du fragment, la valeur par défaut est de 1 Mo. Ceci peut être ajusté en fonction de la situation réelle : si le fragment est trop petit, le nombre de requêtes sera augmenté, et si le fragment est trop grand, il en retransmettra beaucoup en cas d'échec du téléchargement.

5. simultanéUploads : le nombre de blocs de téléchargement simultanés, la valeur par défaut est 3, qui peut être ajusté selon les besoins.

7.2.3, méthode checkChunkUploadedByResponse (important)

La fonction de cette méthode est de vérifier si le fragment actuellement téléchargé a été téléchargé avec succès.

Cette méthode n'appellera qu'une seule fois la requête get fournie par l'interface de téléchargement pour obtenir toutes les informations de fragment de ce fichier depuis le backend. Elle a deux paramètres :

chunk : informations sur le fragment actuel, y compris des données telles que le décalage.

message : appelle toutes les informations de fragmentation renvoyées par la requête get fournie par l'interface de téléchargement.

Si cette méthode renvoie vrai, cela signifie que le segment actuel a été téléchargé avec succès, il n'est alors pas nécessaire d'appeler la méthode de téléchargement pour envoyer une demande de post-téléchargement.

À l'intérieur de la fonction, le message de contenu de réponse renvoyé par le serveur est d'abord analysé dans un objet JSON.

let messageObj = JSON.parse(message);

Retirez ensuite le champ de données en tant qu'objet de données.

let dataObj = messageObj.data;

Ensuite, jugez si le champ téléchargé est inclus dans l'objet de données. S'il y a une valeur de téléchargement directement renvoyée, le champ téléchargé est généralement un signe du serveur indiquant si le téléchargement du fragment est terminé.

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

Si la liste des pièces téléchargées est : [1, 2], c'est-à-dire que les parties 1 et 2 ont été téléchargées avec succès, et que les parties 3 et 4 n'ont pas encore été téléchargées.

Le bloc actuel à vérifier est le bloc 2, offset=1.

Le processus d'exécution est alors :

1. La valeur de dataObj.uploadedChunks est [1, 2].

2. La valeur de chunk.offset est toujours 1.

3. Chunk.offset+1 calcule que le numéro de séquence du bloc 2 est 2.

4. indexOf(2) recherche la valeur 2 dans le tableau [1, 2].

5. La valeur d'index renvoyée est 1, c'est-à-dire trouvée.

Donc pour la partie 2, retournez true, indiquant que la partie a été téléchargée avec succès.

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

Le but du retour est de vérifier si le bloc actuel a été téléchargé avec succès en consultant la liste des blocs téléchargés renvoyée par le serveur, sinon continuez à appeler la demande de publication fournie par upload pour télécharger.

7.2.4, méthode parseTimeRemaining

Méthode pour formater le temps de téléchargement restant. L'heure d'origine sera au format xx jours xx heures xx minutes xx secondes.

code clé:

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

Par conséquent, ces configurations sont principalement destinées au téléchargement en morceaux et améliorent l'efficacité et l'expérience de téléchargement de fichiers volumineux grâce à des mécanismes tels que le regroupement, la concurrence et la vérification.

code clé:

    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. Autres paramètres

Rôle du paramètre :

1. fileStatus : définissez la description textuelle de l'état de téléchargement, qui sera utilisée dans le composant qui affiche l'état de téléchargement.

2. uploadFileList : la liste des fichiers qui ont été téléchargés avec succès, qui peut être utilisée pour enregistrer les informations des fichiers téléchargés avec succès.

code clé:

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

7.2.6, méthode onFileAdded (important)

Logique spécifique :

1. Ajoutez le fichier ajouté à uploadFileList.

2. Après avoir calculé MD5, appelez file.resume().

3. file.resume() construira une requête GET en interne, incluant les informations MD5 du fichier, et l'enverra au serveur.

4. Après avoir reçu la demande, le serveur vérifie si le fichier existe selon MD5.

5. S'il existe, renvoyez les informations de bloc téléchargées.

6. Une fois que le frontal a reçu la réponse, il appelle la logique checkChunkUploadedByResponse.

7. checkChunkUploadedByResponse vérifie l'état de téléchargement de chaque morceau en fonction des données de réponse et juge quels morceaux ont été téléchargés et lesquels ne l'ont pas été en fonction de la réponse.

8. Téléchargez les morceaux non téléchargés. Pour les morceaux non téléchargés, file.resume() continuera à déclencher la demande (POST) pour télécharger le morceau jusqu'à ce que tous les morceaux soient vérifiés et téléchargés.

Ainsi, la méthode file.resume() inclut non seulement la logique de vérification de checkChunkUploadedByResponse, mais également la logique de déclenchement du téléchargement réel de la pièce.

code clé:

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

7.2.7, méthode getFileMD5 (important)

Calculez la valeur MD5 du fichier avant le téléchargement et effectuez le calcul de l'accumulation MD5 pendant le découpage. La logique générale est la suivante :

Initialisez d'abord l'objet SparkMD5 Spark pour enregistrer la valeur cumulée de MD5, puis lisez le contenu du fichier en blocs via FileReader, chaque fois en lisant une tranche de la taille d'un morceau, lors de la lecture de chaque tranche, ajoutez le contenu de la tranche à l'objet Spark, effectuez MD5 accumulation, puis répétez le processus jusqu’à ce que toutes les tranches soient lues. Enfin, appelez spark.end() pour obtenir la valeur MD5 globale calculée de manière cumulative du fichier.

Logique spécifique :

1. Créez l'objet spark SparkMD5 pour calculer la valeur MD5.

let spark = new SparkMD5.ArrayBuffer();

2. Créez un objet FileReader fileReader pour lire le contenu du fichier.

let fileReader = new FileReader();

3. Obtenez l'implémentation compatible de la méthode slice de l'objet File, responsable de l'obtention de la méthode slice du fichier.

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

4. Calculez le nombre total de morceaux et la taille de chaque morceau est CHUNK_SIZE.

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

5. Enregistrez l'heure de début startTime.

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

6. Suspendez le téléchargement du fichier file.pause().

file.pause();

7. La fonction loadNext est utilisée pour charger des fragments de fichiers.

Logique spécifique :

La première étape consiste à calculer la position de départ du fragment actuel, qui est le numéro de série du fragment actuel currentChunk multiplié par la taille du fragment CHUNK_SIZE.

La deuxième étape consiste à calculer la position de fin du fragment actuel : si début + taille du fragment est supérieur ou égal à la taille totale file.size, alors fin est la taille totale file.size, sinon fin est début + taille du fragment CHUNK_SIZE. .

La troisième étape consiste à obtenir un objet slice Blob d'un fichier grâce à la méthode blobSlice, qui intercepte la partie du début à la fin du fichier. En fait, il s'agit d'appeler la méthode de découpage, de transmettre l'objet et la plage du fichier et d'effectuer l'opération de découpage du fichier.

Étape 4 : Appelez la méthode readAsArrayBuffer de FileReader pour lire le contenu de l’objet fragment Blob.

Étape 5 : readAsArrayBuffer déclenchera l'événement onload de FileReader et le contenu du fragment pourra être obtenu et traité ultérieurement lors du rappel de l'événement.

Ainsi, la fonction de loadNext est d'obtenir un fragment Blob du fichier spécifié en fonction de la taille du fragment et du numéro de série, puis d'utiliser FileReader pour lire le contenu ArrayBuffer de ce fragment.

code clé:

    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. Ce code est la fonction de rappel de l'événement onload de FileReader, qui est utilisé pour traiter le calcul de l'accumulation MD5 après avoir lu le contenu du fragment.

Logique spécifique :

La première étape consiste à ajouter le contenu ArrayBuffer fragmenté lu pour déclencher spark.append (e.target.result) et à effectuer une accumulation MD5.

La deuxième étape consiste à juger que si le currentChunk du numéro de fragment est encore plus petit que le nombre total de fragments, cela signifie qu'il reste des fragments non lus : currentChunk est incrémenté automatiquement et loadNext() est appelé pour charger le fragment suivant.

La troisième étape, sinon, signifie que tous les fragments ont été lus : calculez la valeur MD5 finale md5 via spark et end(), imprimez et calculez cela prend du temps, et renvoyez la valeur md5 via un rappel.

Quatrième étape, la fonction de rappel recevra md5 de l'extérieur, jugera si le fichier est dupliqué et déterminera la logique de suivi.

La fonction de ce code est donc de lire récursivement les fragments de fichier, d'accumuler et de calculer MD5, et enfin de renvoyer le résultat MD5, complétant ainsi la vérification MD5 de l'ensemble du fichier. En chargeant par morceaux, les problèmes de performances causés par le chargement de l'intégralité du fichier volumineux en une seule fois sont évités et un calcul MD5 efficace est réalisé.

code clé:

      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, code complet

<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>

Huit, lancez le projet

Ensuite, laissez-moi exécuter ce projet et vous montrer l'effet.

8.1. Télécharger en morceaux

J'ai préparé à l'avance un fichier d'une taille de 141,1 Mo pour le téléchargement, et il a été coupé en 2 morceaux par défaut pour le téléchargement.

Ce sont les informations écrites dans la base de données :

Informations sur les fragments :

Informations sur le dossier :

Vous vous demandez pourquoi le fichier 141M n'est pas coupé en 3 morceaux, mais seulement en 2 morceaux, et la taille du deuxième morceau est de 91 Mo. Laissez-moi vous expliquer la raison en détail ci-dessous : 

En effet, théoriquement, le fichier 141 Mo sera divisé en trois fichiers fragmentés de 50 Mo, 50 Mo et 41 Mo, mais les derniers 41 Mo n'atteignent pas 50 Mo. Les 3 blocs sont fusionnés pour réduire le nombre de fractionnements, les tranches finales font donc 50 Mo. et 91 Mo.

8.2. Reprise du point d'arrêt 

Parfois, il peut y avoir des raisons de réseau ou le client souhaite enregistrer le reste et le télécharger demain. 

La prochaine fois, la dernière progression du téléchargement sera conservée jusqu'à ce que le téléchargement soit réussi ! 

Voici les informations écrites dans la base de données :

Informations sur les fragments :

Parce que seule une partie des informations sur le fragment est téléchargée, mais elles seront enregistrées par la base de données. 

Informations sur le dossier :

Étant donné que le fichier n'a pas été entièrement téléchargé, le tableau d'informations sur le fichier ne stockera pas les informations sur le fichier. 

La prochaine fois, nous continuerons à télécharger en fonction de la progression de la dernière fois.

Voici les informations écrites dans la base de données : 

Informations sur les fragments :

Informations sur le dossier :

8.3. Deuxième passage 

C'est très simple à télécharger en quelques secondes, j'ai choisi de télécharger le gros fichier tout à l'heure. 

Évidemment, aucun découpage n'a été effectué, seule l'interface de vérification a été appelée une fois, et toutes les informations de fragmentation de ce fichier ont été interrogées à partir de la base de données basée sur MD5 et renvoyées au front-end, par rapport aux informations de fragmentation du front-end, c'était vérifié que le fichier avait été complètement téléchargé, le téléchargement est donc terminé en 1 seconde ! 

9. Adresse du code source du Gitee

Ici, j'ouvre le code complet du projet, pour que vous puissiez apprendre par vous-même !

Front-end : Vue implémente le transfert de fichiers volumineux en quelques secondes + transfert de reprise du point d'arrêt + téléchargement de fragments

Back-end : SpringBoot implémente la transmission de fichiers volumineux en quelques secondes + reprise de la transmission du point d'arrêt + téléchargement de fragments

10. Résumé

Ce qui précède est mon explication personnelle du processus complet de réalisation par Vue+SpringBoot du transfert de fichiers volumineux en quelques secondes, du transfert de reprise du point d'arrêt et du téléchargement de fragments. Presque tous les endroits sont en place. Si vous avez des questions, n'hésitez pas à en discuter dans la zone de commentaires !

Je suppose que tu aimes

Origine blog.csdn.net/HJW_233/article/details/132224072
conseillé
Classement