【千語長文】Vue+SpringBootで大容量ファイルの2回目送信、ブレークポイント再開送信、フラグメントアップロードを実現する完全チュートリアル(Giteeソースコード提供)

前書き: 最近、実際のプロジェクトで要件に遭遇しました。顧客は比較的大きなファイルをアップロードすることがあります。従来のファイル アップロード ソリューションを使用すると、サーバーへの高い負荷、リソースの浪費、さらにはメモリ オーバーフローなどのセキュリティ リスクが発生する可能性があります。そのため、一連の問題を解決するために、大きなファイルのアップロードを実現するには新しい技術的解決策が必要であるということですが、暇なときにインターネット上の関連チュートリアルを参照し、最終的には自分でまとめています。大容量ファイルを実現する方法をステップごとに説明します 2 回目の送信、ブレークポイント再開送信、フラグメンテーションの 3 つの機能について、各コードを説明します ブログの最後に、Gitee のソース コードを皆さんに提供しますダウンロードする。

目次

1. この技術ソリューションを使用する理由 

2. 即時送信とは

3. ブレークポイントからの再開とは 

4. マルチパートアップロードとは

5. アップロードプロセス

6. SpringBoot プロジェクトをビルドする

6.1. 準備 

6.1.1. pom 依存関係のインポート

6.1.2、yml設定ファイル

6.1.3、mybatis-config.xml 設定ファイル

6.1.4、SQLファイル

6.2、定数クラス 

6.2.1、HttpStatus の共通戻りステータス コード定数

6.3、エンティティクラス

6.3.1、AjaxResult の統合結果のカプセル化

6.3.2、ファイルスライスクラス

6.3.3、ファイル情報クラス

6.4、GlobalCorsConfig グローバル CORS クロスドメイン構成

6.5、永続化レイヤー

6.5.1、FileChunkMapper.xml ファイル

6.5.2、FileChunkMapper ファイル

6.5.3、FileInfoMapper.xml ファイル

6.5.4、FileInfoMapperファイル

6.6. ファイルアップロードコアサービスクラス(重要)

6.6.1、ファイルアップロードチェックのロジック

6.6.2. フラグメントアップロードのコアロジックを実現する

6.6.3. ファイルの断片化処理

6.6.4、完全なコード

6.7、FileControllerリクエスト層

7. Vue プロジェクトをビルドする

7.1. 準備

7.2、ホームビューページ

7.2.1. アップローダーコンポーネントの使用

7.2.2. データデータの初期化

7.2.3、checkChunkUploadedByResponseメソッド(重要)

7.2.4、parseTimeRemainingメソッド

7.2.5. その他のパラメータ

7.2.6、onFileAdded メソッド (重要)

7.2.7、getFileMD5メソッド(重要)

7.2.8、完全なコード

8. プロジェクトを実行します

8.1. 分割してアップロードする

8.2. ブレークポイントの再開 

8.3. 2回目のパス 

9.Giteeソースコードアドレス

10. まとめ


1. この技術ソリューションを使用する理由 

フロントエンドが一度に非常に大きなファイル (1G など) をアップロードし、断片化/ブレークポイント再開送信などの技術的ソリューションを採用していない場合、主に次のような隠れた危険や問題に直面します。

1.ネットワークの通信速度が遅い

アップロード時間は長く、ファイルを完全にアップロードするには継続的で安定したアップストリーム帯域幅を占有する必要があります。ネットワークの状態が良好でない場合、アップロードは非常に遅くなり、ユーザー エクスペリエンスが損なわれます。

2.途中で失敗した場合は再アップロードが必要です

ネットワークなどの理由でアップロード処理が中断された場合、送信全体が失敗します。これには、ユーザーが完全なファイルを再度アップロードするという作業を繰り返す必要があるだけです。

3. サーバーに大きな負荷がかかっています

サーバーは大きなファイルを継続的に処理するために多くのリソースを占有する必要があるため、サーバーのパフォーマンスに多大な負荷がかかり、他のサービスに影響を与える可能性があります。

4. 交通資源の無駄

大きなファイルを一度に完全にアップロードすると、同じファイルがすでに存在すると、繰り返し大量のネットワーク トラフィックが消費され、データの無駄になります。

5. アップロードの進行状況を確認するのが難しい

ユーザーはアップロードの進行状況を認識できず、アップロードが失敗した場合はどれだけのデータがアップロードされたかわかりません。

したがって、これらの問題を解決するには、フラグメンテーションや伝送再開などの技術を活用することが非常に重要です。データ ブロックをバッチでアップロードできるため、一度にすべてをアップロードするデメリットを回避できます。同時に、アップロードされたフラグメントの検証や記録などの方法と組み合わせることで、アップロードプロセス全体を制御可能、回復可能にし、トラフィックを節約し、伝送効率を大幅に向上させることができます。

2. 即時送信とは

このプロジェクトを使ってMiaochuanの実装ロジックを分かりやすく解説していきます。

1.クライアントVue がファイルをアップロードするとき、最初にファイルのMD5値を計算し、次にその MD5値をSpringBoot サーバーに送信します。

2. SpringBootサーバーは MD5値を受信するとMyBatisを使用してMySQLデータベースにクエリを実行し、同じ MD5 値を持つファイルが既に存在するかどうかを確認します。

3.存在する場合はファイルがアップロードされたことを意味し、サーバーはファイル内にどのフラグメントが存在するかをデータベースから直接問い合わせてクライアントに返します。

4.クライアントはファイルの断片化情報を取得した後、実際のファイルの内容をアップロードせずに完全なファイルを直接組み立てます。

5. MD5値がデータベースに存在しない場合は、ファイルがアップロードされていないことを意味し、サーバーはクライアントがファイル全体をアップロードする必要があることを返します。

6.クライアントがファイルをアップロードした後、サーバーはファイルと MD 5 の間の対応する関係を MySQL データベースに追加し、ファイルの断片化情報を保存します。

7.次回同じファイルをアップロードするときは、MD5 値を使用して数秒でアップロードできます。

したがって、核心は、MySQL データベースを使用して各ファイルのMD5と断片化情報を記録し、アップロード時にMD5を通じてクエリを実行し、MySQL が即時送信を許可するかどうかを判断して、同じファイルの繰り返しアップロードを回避することです。

3. ブレークポイントからの再開とは 

それでは、本プロジェクトにおける送信再開の考え方をわかりやすく説明させていただきます。

1. フロントエンド Vue がファイルをアップロードするとき、ファイルを複数の小さな部分に分割し、毎回 1 つの小さな部分をアップロードします。

2. 小さなブロックがアップロードされるたびに、バックエンド SpringBoot は、小さなブロックのシリアル番号、ファイル MD5、コンテンツ ハッシュなどのこの小さなブロックの情報を記録します。MySQL データベースに保存できます。

3. アップロードが中断された場合、Vue 側は SpringBoot にどの小さな部分がアップロードされたかを問い合わせることができます。

4. SpringBoot はデータベースにクエリを実行し、アップロードされた小さな情報を Vue に返します。

5. Vue は、途中で中断された小さな部分のみをアップロードし続けることができます。

6. SpringBoot は、小さなブロックのシリアル番号とファイルの MD5 に従って、これらの小さなブロックを完全なファイルに再構築します。

7. 今後、ファイルが再度アップロードされた場合、MD5 値を通じてファイルがすでに存在していることがわかり、実際のコンテンツをアップロードせずに、アップロードの成功を直接返します。このように、スライスをアップロードし、アップロードされたスライスの情報を永続的に記録することで、ブレークポイントからアップロードを再開することができます。

重要なのは、アップロードされたスライス情報を記録して取得するためのインターフェイスを SpringBoot が提供する必要があるということです。Vue 側は順番にスライスしてアップロードする必要があります。最後に SpringBoot がファイルをまとめます。簡単に言うと、ネットワークなどの理由でアップロードが中断されます。送信されたデータ量を記録することで、中断後に残りのデータをアップロードし続けるための技術的ソリューション。

4. マルチパートアップロードとは

最後に、このプロジェクトをもとにマルチパートアップロードの概念をわかりやすく説明します。

1. 分割アップロードの目的は、大きなファイルを複数の小さな部分に分割して、同時アップロードを実現し、伝送速度を向上させることです。

2. 大きなファイルは、設定されたフラグメント サイズ (たとえば、50M のフラグメント) に従って分割できます。

3. Vue プロジェクトは、各スプリットを SpringBoot サーバーに順番にアップロードし、次に各部分を順番にアップロードします。

4. SpringBoot サーバーはフラグメントを受信すると、それをローカルに一時的に保存し、フラグメントのシリアル番号、ファイル MD5 などのフラグメントの特性情報を記録し、データベースに書き込むことができます。

5. すべてのフラグメントをアップロードした後、SpringBoot はそれらを順番に元の完全なファイルに再組み立てします。

5. アップロードプロセス

これは私が描いたフロントエンド スライス処理ロジックです。

これは、バックエンドでスライスを処理するために私が描いたロジックです。

これは、プロジェクトの実行プロセス全体を論理的に組み合わせたものです。 

1. 作成したら、アップローダー コンポーネントを初期化し、フラグメント サイズ、アップロード方法、その他の構成を指定します。

2. onFileAdded メソッドで、選択したファイルの MD5 が計算された後、file.resume() を呼び出してアップロードを開始します。

3. file.resume() はまず内部で GET リクエストを送信し、アップロードされたファイルのフラグメントをサーバーに要求します。

4. サーバーは、アップロードされたフラグメントのリストを含む JSON を返します。

5. アップローダー コンポーネントは checkChunkUploadedByResponse を呼び出して、現在のチャンクがアップロードされたリストにあるかどうかを確認します。

6. アップロードされていないセグメントの場合、file.resume() はセグメントをアップロードするための POST リクエストをトリガーし続けます。

7. POST リクエストには、フラグメントのデータやオフセットなどの情報が含まれます。

8. サーバーは断片化されたデータを受信し、それをファイルの指定された場所に書き込み、成功の応答を返します。

9. アップローダー コンポーネントは、セグメントがアップロードされたことを記録します。

10. すべてのフラグメントを順番にアップロードした後、サーバー側はすべてのフラグメントを完全なファイルにマージします。

11. onFileSuccess が呼び出され、アップロードが成功したことが通知されます。

12. このように、アップロードされたフラグメントを問い合わせるGETリクエスト+未完成のフラグメントをアップロードするPOST+検証により、ブレークポイント再開・フラグメントアップロードが実現します。

私が描いたスイムレーンのグラフは次のとおりです。

6. SpringBoot プロジェクトをビルドする

次に、このプロジェクトのバックエンドを構築する方法を段階的に説明します。これは完全なプロジェクトのスクリーンショットです。

6.1. 準備 

6.1.1. pom 依存関係のインポート

これはバックエンドの完全な依存関係情報です。

完全なコード:

<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、yml設定ファイル

メイン設定のバックエンド ポートは 9090、単一ファイルの制限は最大 100MB、MySQL の設定情報および MyBatis の設定情報です。

完全なコード:

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、mybatis-config.xml 設定ファイル

リソースフォルダーの下に新しい mybatis フォルダーを作成し、mybatis の設定ファイルを保存します。

完全なコード:

<?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、SQLファイル

ストアファイルの断片化テーブル:

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;

ストアファイル情報テーブル:


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、定数クラス 

6.2.1、HttpStatus の共通戻りステータス コード定数

完全なコード:

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、エンティティクラス

6.3.1、AjaxResult の統合結果のカプセル化

完全なコード:

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、ファイルスライスクラス

完全なコード:

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、ファイル情報クラス

完全なコード:

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、GlobalCorsConfig グローバル CORS クロスドメイン構成

1. @Configuration アノテーションは、これが構成クラスであることを示します。

2. WebMvcConfigurer インターフェイスは、SpringMVC の構成をカスタマイズするために使用されます。

3. addCorsMappings メソッドは、クロスドメイン アクセス ポリシーを定義するために使用されます。

4. addMapping("/**") は、すべてのリクエスト パスをインターセプトすることを意味します。

5. allowedOriginPatterns("*") は、すべてのドメイン名要求が許可されることを示します。

6.allowCredentials(true) は、Cookie が許可されることを意味します。

7. allowedHeaders は、許可されるリクエスト ヘッダーを示します。

8. allowedMethods は、許可されるリクエストメソッドを示します。

9. maxAge(3600)は、プリフライトリクエストの有効期間が3600秒であることを示します。

これにより、グローバル CORS クロスドメイン構成が実現され、すべてのドメイン名からのリクエストがこのサービスにアクセスできるようになり、リクエストのヘッダーとメソッドは自由に設定でき、有効期限は最大 1 時間になります。

完全なコード:

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、永続化レイヤー

6.5.1、FileChunkMapper.xml ファイル

主に 2 つの SQL を記述します。1 つは md5 暗号化情報に従ってデータベースのすべてのフラグメンテーション情報をクエリするもので、もう 1 つは成功した各フラグメント アップロードのファイル情報を記録するものです。

完全なコード:

<?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、FileChunkMapper ファイル

完全なコード:

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、FileInfoMapper.xml ファイル

完全なコード:

<?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、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. ファイルアップロードコアサービスクラス(重要)

6.6.1、ファイルアップロードチェックのロジック

1. ファイル識別子 (md5) に従って、ファイルが既に存在するかどうかをデータベースから照会します。

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

2. 見つからない場合は、ファイルがアップロードされていないことを意味し、uploaded=false が返されます。

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

3. クエリで 1 つの部分だけが見つかった場合は、ファイル全体がアップロードされたことを意味し、uploaded=true が返されます。

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

4. 複数のデータがクエリされる場合、これは分割してアップロードされた大きなファイルであることを意味します。

5. 断片化されたデータを走査し、各ファイル ブロックの番号を取得して、uploadedFiles 配列に保存します。

6. 最後に、uploadedChunks をフロントエンドに返します。

// 处理分片
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. フロントエンドはデータを取得すると、どのフラグメントが大きなファイルにアップロードされたか、どのフラグメントがアップロードされずに残っているかを認識します。

8. 次に、ブレークポイントでの再開の効果を達成するために、ブレークポイントで残りのフラグメントのアップロードを続けます。そのため、このコードは主にファイル データベースをチェックして、ファイルの全体または一部が既に存在するかどうかを確認し、ファイルが必要かどうかを判断します。完全なファイルをアップロードするか、ブレークポイントのアップロードを続行します。

これにより、アップロードの繰り返しを回避し、伝送効率を向上させることができます。これが、即時伝送と再開可能な伝送を実現するための重要なロジックです。

キーコード:

    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. フラグメントアップロードのコアロジックを実現する

このコードは、RandomAccessFile を使用してフラグメントのアップロードと書き込みのロジックを実現します。

具体的なロジック:

1. RandomAccessFile オブジェクトを作成し、ファイル パスに従って保存し、ファイルをランダムに読み書きできる読み取りおよび書き込みモードでファイルを開きます。

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

2. 各フラグメントのチャンクサイズを計算します。フロントエンドがそれを渡さない場合は、デフォルト値 (50MB、私が定義した定数) を使用します。

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

3. 現在のフラグメントのオフセットを計算します。これは、(フラグメント番号 - 1) * フラグメント サイズによって計算されます。

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

4. フラグメントのオフセット位置を特定するには、seek() メソッドを使用します。

randomAccessFile.seek(offset);

5. write() メソッドを使用して、現在のフラグメントのファイル内容のバイトを書き込みます。

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

6. すべてのシャードが書き込まれるまで、このプロセスを繰り返します。

7. 最後にファイルを閉じます。

randomAccessFile.close();

RandomAccessFile は任意の位置でファイルの読み取りと書き込みができるため、フラグメントの順序で指定された位置に書き込み、フラグメント アップロードの効果を実現できます。この方法では、オペレーティング システムのファイル キャッシュを最大限に活用できるため、より効率的です。各シャードは 1 回だけ書き込まれ、ファイルを読み取ったり変更したりする必要がないため、IO 操作が節約されます。これは、RandomAccessFile を介してフラグメントのアップロードを実装し、各フラグメントを完全なファイルに結合する一般的な方法です。

簡単な実践例を紹介しながら、わかりやすく説明しましょう。

まず、オフセットとは何かを紹介します。オフセットは、ファイル全体における各フラグメントの位置を決定するために使用されます。

このキーコードによると:

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

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

オフセット = フラグメント サイズ * (シャード シーケンス番号 - 1)

230MB のファイルを例に挙げます。

1. チャンクサイズは 50MB です。

2. 最初のフラグメント番号 fileChunk、getChunkNumber() は 1 です。

3. 最初のスライスのオフセット = 50MB * (1 - 1) = 0 となります。

2 番目のスライスのフラグメント番号は 2 であるため、2 番目のスライスのオフセット = 50MB * (2 - 1) = 50MB となります。同様に、各スライスのオフセットを計算できます。

n 番目のスライスのオフセットの式は、オフセット = スライス サイズ * (n 番目のスライスのセグメント番号 - 1) です。

したがって、完全な実行プロセスは次のようになります。

230MB のファイルは 5 つのフラグメントに分割されます。

1. 最初のフラグメント: 50MB、オフセットは 0。

2. 2 番目のフラグメント: 50MB、オフセット 50MB。

3. 3 番目のフラグメント: 50MB、オフセット 100MB。

4. 4 番目のフラグメント: 50MB、オフセット 150MB。

5. 5 番目のフラグメント: 30MB、オフセット 200MB。

クライアントは、次の 5 つのフラグメントを同時にアップロードできます。

1. 最初のフラグメントをアップロードし、オフセット 0 に書き込みます。

2. 2 番目のフラグメントをアップロードし、オフセット 50MB に書き込みます。

3. 3 番目のフラグメントをアップロードし、オフセット 100MB に書き込みます。

4. 4 番目のフラグメントをアップロードし、オフセット 150MB に書き込みます。

5. 5 番目のフラグメントをアップロードし、オフセット 200MB に書き込みます。

サーバー側は、RandomAccessFile を介してオフセットに従って各シャードのコンテンツを直接書き込むことができます。これにより、230MB の大容量ファイルを 50MB のフラグメント 5 つで高速にアップロードできるようになり、フラグメントアップロードの効果が実感でき、通信速度が向上します。

キーコード:

    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. ファイルの断片化処理

ここで最も重要なことは、ファイル フラグメントのランダムな読み取りと書き込みを実装し、アップロードされたファイル フラグメント データを正しいオフセット位置に書き込むことができる、uploadFileByRandomAccessFile メソッドです。 

ファイルの一意の識別子 (MD5) に従ってデータベース内のフラグメントの総数をクエリします。それがフロントエンド スライスの総数と等しい場合、大きなファイルがアップロードされたことを意味し、完全なファイル情報が返されます。今回アップロードされたファイルはデータベースに保存されます。

キーコード:

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、完全なコード

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、FileControllerリクエスト層

記述は非常に簡潔です。これらがすべて同じ /upload インターフェイスである理由、リクエスト メソッドがそれぞれ get と post であるのは、アップローダー コンポーネントがチェック時に最初に /upload の get リクエストを呼び出し、次にフラグメントをアップロードして、 /upload のポストリクエスト。

1. まず GET リクエストを使用してファイルが存在するかどうかを確認し、インスタント送信、ブレークポイント再開送信、またはマルチパート アップロードのいずれを実装するかを決定します。

2. 次に、クライアントは POST リクエストを使用して各フラグメントを順番にアップロードし、サーバーはフラグメントを受信した後にファイルを書き込みます。

完全なコード:

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. Vue プロジェクトをビルドする

ここでは Vue2.0 を使用しています。これはプロジェクトの完全なスクリーンショットです。

7.1. 準備

1.アップローダーをインストールする

npm install --save vue-simple-uploader

2.spark-md5によると

npm install --save spark-md5

3. main.js にコンポーネントを導入する

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

7.2、ホームビューページ

実際、ここでの主なロジックは、まず getFileMD5 メソッドを使用してこのファイルの MD5 チェック コードをスライスして生成し、次にそのファイルを使用し、現在のファイルのアップロードされたすべてのフラグメント情報を取得し、次に checkChunkUploadedByResponse メソッドのロジックを使用することです。どのフラグメントをまだアップロードする必要があるかを判断します。

7.2.1. アップローダーコンポーネントの使用

このコンポーネントは、アップロード コンポーネントであるサードパーティ ライブラリ vue-uploader を使用します。

パラメータの説明:

1. ref: コンポーネントに参照名を付けます。ここはアップローダーです。

2. オプション: アップロード アドレス、受け入れられるファイルの種類などのアップロード オプション。

3. autoStart: アップロードを自動的に開始するかどうか。デフォルトは true ですが、ここでは false に設定されています。

4. fileStatusText: カスタムのアップロード ステータス プロンプト テキスト。

5. @file-added: ファイル追加時のフック。

6. @file-success: ファイルが正常にアップロードされたときにフックします。

7. @file-error: ファイルのアップロードが失敗した場合にフックします。

8. @file-progress: ファイルのアップロードの進行状況をフックします。

3 つのサブコンポーネントが含まれています。

1.uploader-unsupport: ブラウザがサポートしていない場合は、このコンポーネントを表示します。

2. Uploader-drop: アップロード領域コンポーネントをドラッグ アンド ドロップして、コンテンツをカスタマイズします。

3. Uploader-btn: アップロード ボタン コンポーネント。

4. Uploader-files: アップロードするファイルを選択したリスト コンポーネント。

したがって、このコンポーネントは、ファイルの選択、アップロードのドラッグ アンド ドロップ、アップロードの進行状況の表示、アップロード結果のコールバックなど、完全なファイル アップロード機能を実装します。オプションを使用してアップロード パラメーターを構成し、さまざまなフックを使用してアップロード結果を処理し、アップロード領域のスタイルをカスタマイズして完全なアップロード コンポーネントを実装できます。

完全なコード:

<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. データデータの初期化

まずMD5ツールを導入し、事前にフラグメントサイズを初期化します。

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

オプションパラメータ:

1. ターゲット: アップロードされたインターフェースのアドレス

2. testChunks: マルチパートアップロード検証を有効にするかどうか。部分アップロードとは、ファイルを細かく分割して同時にアップロードすることで、大きなファイルのアップロードを効率化することができます。

3. UploadMethod: アップロード メソッド。デフォルトは POST です。

4. chunkSize: フラグメント サイズ。デフォルトは 1MB です。フラグメントが小さすぎるとリクエストの数が増加し、フラグメントが大きすぎるとアップロードに失敗した場合の再送信が多くなり、実際の状況に応じて調整できます。

5. 同時アップロード: 同時アップロード ブロックの数。デフォルトは 3 ですが、必要に応じて調整できます。

7.2.3、checkChunkUploadedByResponseメソッド(重要)

このメソッドの機能は、現在アップロードされているフラグメントが正常にアップロードされたかどうかを確認することです。

このメソッドは、アップロード インターフェイスによって提供される get リクエストを 1 回だけ呼び出して、バックエンドからこのファイルのすべてのフラグメント情報を取得します。これには 2 つのパラメータがあります。

チャンク: オフセットなどのデータを含む、現在のフラグメントに関する情報。

メッセージ: アップロード インターフェイスによって提供される get リクエストによって返されるすべての断片化情報を呼び出します。

このメソッドが true を返した場合、現在のセグメントが正常にアップロードされたことを意味するため、アップロード メソッドを呼び出してポスト アップロード リクエストを送信する必要はありません。

関数内では、サーバーから返された応答コンテンツ メッセージがまず JSON オブジェクトに解析されます。

let messageObj = JSON.parse(message);

次に、データフィールドをデータオブジェクトとして取り出します。

let dataObj = messageObj.data;

次に、uploaded フィールドがデータオブジェクトに含まれているかどうかを判断し、uploaded の値が直接返された場合、uploaded フィールドは通常、フラグメントのアップロードが完了したかどうかを示すサーバーの兆候です。

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

アップロードされたパート リストが [1, 2] の場合、パート 1 とパート 2 は正常にアップロードされていますが、パート 3 とパート 4 はまだアップロードされていません。

検証される現在のブロックはブロック 2、オフセット = 1 です。

次に、実行プロセスは次のようになります。

1. dataObj.uploadedChunks の値は [1, 2] です。

2. chunk.offset の値は 1 のままです。

3. Chunk.offset+1 は、ブロック 2 のシーケンス番号が 2 であると計算します。

4.indexOf(2) は、配列 [1, 2] 内の値 2 を検索します。

5. 返されるインデックス値は 1、つまり見つかったものです。

したがって、パート 2 では、パートが正常にアップロードされたことを示す true を返します。

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

return の目的は、サーバーから返されたアップロードされたブロックのリストを見て、現在のブロックが正常にアップロードされたかどうかを確認することです。そうでない場合は、upload によって提供された post リクエストを呼び出してアップロードし続けることです。

7.2.4、parseTimeRemainingメソッド

残りのアップロード時間をフォーマットするメソッド。元の時刻は、xx 日 xx 時間 xx 分 xx 秒の形式で表示されます。

キーコード:

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

したがって、これらの構成は主に分割してアップロードするためのものであり、チャンキング、同時実行、検証などのメカニズムを通じて大きなファイルをアップロードする効率とエクスペリエンスが向上します。

キーコード:

    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.その他のパラメータ

パラメータの役割:

1. fileStatus: アップロード ステータスを表示するコンポーネントで使用される、アップロード ステータスのテキスト説明を定義します。

2. UploadFileList: 正常にアップロードされたファイルのリスト。正常にアップロードされたファイルの情報を保存するために使用できます。

キーコード:

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

7.2.6、onFileAdded メソッド (重要)

具体的なロジック:

1. 追加したファイルをuploadFileListに追加します。

2. MD5 を計算した後、file.resume() を呼び出します。

3. file.resume() は、ファイルの MD5 情報を含む GET リクエストを内部で構築し、サーバーに送信します。

4. リクエストを受信したサーバーは、MD5 に従ってファイルが存在するかどうかを確認します。

5. 存在する場合は、アップロードされたブロック情報を返します。

6. フロントエンドは応答を受信した後、checkChunkUploadedByResponse ロジックを呼び出します。

7. checkChunkUploadedByResponse は、応答データに従って各チャンクのアップロード状況を確認し、応答に従ってどのチャンクがアップロードされ、どのチャンクがアップロードされていないかを判断します。

8. アンアップロードされたチャンクをアップロードする アンアップロードされたピースの場合、すべてのピースが検証されアップロードされるまで、 file.resume() はピースをアップロードするリクエスト (POST) をトリガーし続けます。

したがって、file.resume() メソッドには、checkChunkUploadedByResponse の検証ロジックだけでなく、実際のパーツのアップロードをトリガーするロジックも含まれています。

キーコード:

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

7.2.7、getFileMD5メソッド(重要)

アップロード前にファイルの MD5 値を計算し、スライス中に MD5 の累積計算を実行する一般的なロジックは次のとおりです。

まず、SparkMD5オブジェクトsparkを初期化してMD5の累積値を保存し、次にFileReaderを通じてファイルのコンテンツをブロック単位で読み取り、チャンクサイズのスライスを読み取るたびに、各スライスを読み取るときにスライスのコンテンツをsparkオブジェクトに追加し、MD5を実行します。蓄積し、すべてのスライスが読み取られるまでプロセスを繰り返します。最後に、spark.end() を呼び出して、累積的に計算されたファイル全体の MD5 値を取得します。

具体的なロジック:

1. MD5 値を計算するための SparkMD5 オブジェクト スパークを作成します。

let spark = new SparkMD5.ArrayBuffer();

2. ファイルの内容を読み取るための FileReader オブジェクト fileReader を作成します。

let fileReader = new FileReader();

3. ファイルのスライス メソッドの取得を担当する、File オブジェクトのスライス メソッドの互換性のある実装を取得します。

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

4. チャンクの合計数を計算します。各チャンクのサイズは CHUNK_SIZE です。

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

5. 開始時刻 startTime を記録します。

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

6. ファイル file.pause() のアップロードを一時停止します。

file.pause();

7. ファイルのフラグメントをロードするには、loadNext 関数を使用します。

具体的なロジック:

最初のステップは、現在のフラグメントの開始位置 start を計算することです。これは、現在のフラグメントのシリアル番号 currentChunk にフラグメント サイズ CHUNK_SIZE を乗算したものです。

2 番目のステップは、現在のフラグメントの終了位置を計算することです。start + フラグメント サイズが合計サイズ file.size 以上の場合、end は合計サイズ file.size になります。それ以外の場合、end は start + フラグメント サイズ CHUNK_SIZE になります。 。

3 番目のステップでは、ファイルの先頭から末尾までの部分をインターセプトする blobSlice メソッドを通じてファイルのスライス Blob オブジェクトを取得します。実際には、スライス メソッドを呼び出し、ファイル オブジェクトと範囲を渡し、実際のファイル スライス操作を実行します。

ステップ 4: FileReader の readAsArrayBuffer メソッドを呼び出して、Blob フラグメント オブジェクトのコンテンツを読み取ります。

ステップ 5. readAsArrayBuffer はFileReader の onload イベントをトリガーし、フラグメントのコンテンツは後でイベント コールバックで取得して処理できます。

したがって、loadNext の機能は、フラグメント サイズとシリアル番号に従って指定されたファイルのフラグメント Blob を取得し、FileReader を使用してこのフラグメントの ArrayBuffer コンテンツを読み取ることです。

キーコード:

    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. このコードは、FileReader の onload イベントのコールバック関数であり、フラグメント コンテンツを読み取った後の MD5 累積計算を処理するために使用されます。

具体的なロジック:

最初のステップは、spark.append(e.target.result) を介して、読み取った断片化された ArrayBuffer コンテンツを Spark に追加し、MD5 蓄積を実行することです。

2 番目のステップは、フラグメント番号の currentChunk がまだチャンクの総数より小さい場合、未読のフラグメントがまだあることを意味すると判断し、currentChunk が自動的にインクリメントされ、loadNext() が呼び出されて次のフラグメントがロードされます。

3 番目のステップは、それ以外の場合は、すべてのフラグメントが読み取られたことを意味します。spark と end() を通じて最終的な MD5 値 md5 を計算し、時間をかけて出力および計算し、コールバックを通じて md5 値を返します。

4番目のステップであるコールバック関数は、外部からmd5を受け取り、ファイルが重複しているかどうかを判断し、後続のロジックを決定します。

したがって、このコードの機能は、ファイルのフラグメントを再帰的に読み取り、MD5 を蓄積して計算し、最後に MD5 の結果を返し、ファイル全体の MD5 検証を完了することです。分割してロードすることで、大きなファイル全体を一度にロードすることによるパフォーマンスの問題が回避され、効率的な MD5 計算が実現されます。

キーコード:

      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、完全なコード

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

8. プロジェクトを実行します

次に、実際にこのプロジェクトを実行して、その効果を見てみましょう。

8.1. 分割してアップロードする

事前にアップロード用に141.1MBのファイルを用意しましたが、デフォルトでは2つに分割されてアップロードされました。

データベースに書き込まれる情報は次のとおりです。

フラグメント情報:

ファイル情報:

141M のファイルがなぜ 3 つに分割されず 2 つに分割され、2 番目のファイルのサイズが 91MB になるのか疑問に思われませんか? 以下にその理由を詳しく説明します。 

これは、理論的には、141M ファイルは 50MB、50MB、41MB の 3 つの断片化されたファイルに分割されることになりますが、最後の 41MB が 50MB に満たないため、分割数を減らすために 3 つのブロックがマージされるため、最終的なスライスは 50MB になります。そして91MB。

8.2. ブレークポイントの再開 

場合によっては、ネットワーク上の理由があったり、お客様が残りを保存して明日アップロードしたいと考えたりすることがあります。 

次回、アップロードが成功するまで、最後のアップロードの進行状況が保持されます。 

データベースに書き込まれる情報は次のとおりです。

フラグメント情報:

アップロードされるのはシャード情報の一部だけですが、データベースに記録されるためです。 

ファイル情報:

ファイルが完全にアップロードされていないため、ファイル情報テーブルにはファイル情報が格納されません。 

次回も前回の進捗に合わせてアップしていきます。

データベースに書き込まれる情報は次のとおりです。 

フラグメント情報:

ファイル情報:

8.3. 2回目のパス 

アップロードは非常に簡単で数秒で完了します。ここでは大きなファイルをアップロードすることにします。 

明らかに、スライスは実行されず、検証インターフェイスが 1 回だけ呼び出され、このファイルのすべてのシャード情報が MD5 に基づいてデータベースからクエリされてフロントエンドに返され、フロントエンドのシャード情報と比較すると、ファイルが完全にアップロードされたことを確認したため、アップロードは 1 秒以内に完了しました。 

9.Giteeソースコードアドレス

ここではプロジェクトの完全なコードをオープンソースとして公開しているので、自分で学ぶことができます。

フロントエンド: Vue は数秒で大きなファイル転送 + ブレークポイント再開転送 + フラグメントアップロードを実装します

バックエンド: SpringBoot は数秒で大規模ファイル送信 + ブレークポイント再開送信 + フラグメントアップロードを実装します

10. まとめ

上記は、Vue+SpringBoot による数秒での大規模ファイル転送、ブレークポイント再開転送、およびフラグメントのアップロードを実現する完全なプロセスについての私の個人的な説明です。ほぼすべての場所が用意されています。ご質問がある場合は、コメント エリアで議論してください。 !

おすすめ

転載: blog.csdn.net/HJW_233/article/details/132224072