Java を使用してファイルのブレークポイント再開機能を実装するにはどうすればよいですか?

王林
リリース: 2023-04-27 15:22:07
転載
1841 人が閲覧しました

再開可能なアップロードとは

ユーザーが大きなファイルをアップロードするとき、ネットワーク接続が悪い場合、数時間かかることがあります。回線が中断された場合、再開可能なアップロードを備えていないサーバーは、ファイルを最初から再送信することしかできません。 「再開」をクリックすると、アップロードが切断されたところから送信を継続できるため、ユーザーの不安が大幅に軽減されます。

  • 大きなファイルをアップロードするためのサーバー メモリ不足の問題を解決する

  • 他の要因によりアップロードが終了し、その後もアップロードが継続する問題を解決するブラウザを更新する アップロードを再開でき、ブラウザを再起動した後もアップロードを続行でき (ブラウザを閉じてから再度開きます)、コンピュータを再起動した後もアップロードを続行できます。

    ## アップロード プロセス中のネットワーク変動によるファイルのコンテンツを検出します。 ファイルが失われた場合は、自動的に検出して再アップロードする必要があります。
  • 解決策

フロントエンド

アップロードされたファイルを分割する必要があります
  • ファイルのシーケンス番号を指定する必要があります断片化されたファイルをアップロードしました
  • #アップロードの進行状況を監視し、進行状況バーを制御する必要があります

  • アップロード後、マージ リクエストを送信する必要があります

  • Blob オブジェクト、操作ファイル

バックエンド

フラグメントをアップロードするためのインターフェイス

  • フラグメントをマージするためのインターフェイス

  • フラグメントを取得するためのインターフェイス

  • 他のツールとメソッドを使用して支援します

  • フロントエンドで注意する必要があるのは、ファイルのカットと進行状況バーです。

  • バックエンドで注意する必要があるのは、シャードの保存場所とマージ方法です。シャード

エフェクトのデモンストレーション

まず、アップロードする必要があるファイルを見つけます

アップロードを開始すると、進行状況バーが変わります。クリックしてアップロードを停止すると、進行状況バーが停止します。

Java を使用してファイルのブレークポイント再開機能を実装するにはどうすればよいですか?

バックエンドは、ファイル名とファイル サイズを通じて MD5 を生成します。対応するディレクトリの結果は次のとおりです:

Java を使用してファイルのブレークポイント再開機能を実装するにはどうすればよいですか?

#現在のエンドでアップロードされたファイルが 100% に達すると、ファイル マージ リクエストが送信され、バックエンドのすべてのフラグメントが 1 つのファイルにマージされます。

Java を使用してファイルのブレークポイント再開機能を実装するにはどうすればよいですか?

##下の図から、すべてのシャードが消え、1 つのファイルがマージされたことがわかります

Java を使用してファイルのブレークポイント再開機能を実装するにはどうすればよいですか?

ファイル アップロード プロセス中に、ネットワークの変動によりストリームの一部が失われました (比較サイズ)

Java を使用してファイルのブレークポイント再開機能を実装するにはどうすればよいですか?

    ファイル アップロード プロセス中に、サーバーがフラグメントを失いました (比較フラグメントの連続性)
  • ファイルは改ざんされています (サイズ比較)
  • ##検証コア コード

リファレンス コードフロントエンド

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>

    <h2>html5大文件断点切割上传</h2>
    <div id="progressBar"></div>

    <input id="file" name="mov" type="file" />
    <input id="btn" type="button" value="点我上传" />
    <input id="btn1" type="button" value="点我停止上传" />

    <script type="module">
        import FileSliceUpload  from &#39;../jsutils/FileSliceUpload.js&#39;
        let testingUrl="http://localhost:7003/fileslice/testing"
        let uploadUrl="http://localhost:7003/fileslice/uploads"
        let margeUrl="http://localhost:7003/fileslice/merge-file-slice"
        let progressUrl="http://localhost:7003/fileslice/progress"
         let fileSliceUpload=  new FileSliceUpload(testingUrl,uploadUrl,margeUrl,progressUrl,"#file")
         fileSliceUpload.addProgress("#progressBar")
          let btn=  document.querySelector("#btn")
          let btn1=  document.querySelector("#btn1")
        btn.addEventListener("click",function () {
            fileSliceUpload.startUploadFile()
        })
        btn1.addEventListener("click",function () {
            fileSliceUpload.stopUploadFile()
        })

    </script>


</body>

</html>
ログイン後にコピー
//大文件分片上传,比如10G的压缩包,或者视频等,这些文件太大了  (需要后端配合进行)
class FileSliceUpload{
     
    constructor(testingUrl, uploadUrl, margeUrl,progressUrl, fileSelect) {
            this.testingUrl = testingUrl; // 检测文件上传的url
            this.uploadUrl = uploadUrl;//文件上传接口
            this.margeUrl = margeUrl; // 合并文件接口
            this.progressUrl = progressUrl; //进度接口
            this.fileSelect = fileSelect;
            this.fileObj = null;
            this.totalize = null;
            this.blockSize = 1024 * 1024; //每次上传多少字节1mb(最佳)
            this.sta = 0; //起始位置
            this.end =  this.sta +  this.blockSize; //结束位置
            this.count = 0; //分片个数
            this.barId = "bar"; //进度条id
            this.progressId = "progress";//进度数值ID
            this.fileSliceName = ""; //分片文件名称
            this.fileName = "";
            this.uploadFileInterval = null;  //上传文件定时器

    }

    /**
     *  样式可以进行修改
     * @param {*} progressId   需要将进度条添加到那个元素下面
     */
    addProgress (progressSelect) {
        let bar = document.createElement("div")
        bar.setAttribute("id", this.barId);
        let num = document.createElement("div")
        num.setAttribute("id", this.progressId);
        num.innerText = "0%"
        bar.appendChild(num);
        document.querySelector(progressSelect).appendChild(bar)
     
    }
    //续传  在上传前先去服务器检测之前是否有上传过这个文件,如果还有返回上传的的分片,那么进行续传
    // 将当前服务器上传的最后一个分片会从新上传, 避免因为网络的原因导致分片损坏 
    sequelFile () {
        if (this.fileName) {
            var xhr = new XMLHttpRequest();
            //同步
            xhr.open(&#39;GET&#39;, this.testingUrl + "/" + this.fileName+ "/" + this.blockSize+ "/" + this.totalize, false);
            xhr.send();
            if (xhr.readyState === 4 && xhr.status === 200) {
                let ret = JSON.parse(xhr.response)
                if (ret.code == 20000) {
                   let data= ret.data
                    this.count = data.code;
                    this.fileSliceName = data.fileSliceName
                    //计算起始位置和结束位置
                    this.sta = this.blockSize * this.count
                    //计算结束位置
                    this.end = this.sta + this.blockSize
                } else {
                    this.sta = 0; //从头开始
                    this.end = this.sta + this.blockSize;
                    this.count = 0; //分片个数
                }
            }
        }
    }

    stopUploadFile () {
        clearInterval(this.uploadFileInterval)
    }

    // 文件上传(单文件)
    startUploadFile () { 
         // 进度条
         let bar = document.getElementById(this.barId)
         let progressEl = document.getElementById(this.progressId)
        this.fileObj = document.querySelector(this.fileSelect).files[0];
        this.totalize = this.fileObj.size;
        this.fileName = this.fileObj.name;
 
        //查询是否存在之前上传过此文件,然后继续
        this.sequelFile()
        let ref = this; //拿到当前对象的引用,因为是在异步中使用this就是他本身而不是class
        this.uploadFileInterval = setInterval(function () {
                if (ref.sta > ref.totalize) {
                    //上传完毕后结束定时器
                    clearInterval(ref.uploadFileInterval)
                    //发送合并请求
                    ref.margeUploadFile ()
                    console.log("stop" + ref.sta);
                    return;
                };
                //分片名称
                ref.fileSliceName = ref.fileName + "-slice-" + ref.count++
                //分割文件 ,
                var blob1 =  ref.fileObj.slice(ref.sta, ref.end);
                var fd = new FormData();
                fd.append(&#39;part&#39;, blob1);
                fd.append(&#39;fileSliceName&#39;, ref.fileSliceName);
                fd.append(&#39;fileSize&#39;, ref.totalize);
                var xhr = new XMLHttpRequest();
                xhr.open(&#39;POST&#39;,  ref.uploadUrl, true);
                xhr.send(fd); //异步发送文件,不管是否成功, 会定期检测

                xhr.onreadystatechange = function () {
                    if (xhr.readyState === 4 && xhr.status === 200) {
                        let ret = JSON.parse(xhr.response)
                        if (ret.code == 20000) {
                            //计算进度
                            let percent =  Math.ceil((ret.data*ref.blockSize/ ref.totalize) * 100)
                            if (percent > 100) {
                                percent=100
                                
                            }
                            bar.style.width = percent + &#39;%&#39;;
                            bar.style.backgroundColor = &#39;red&#39;;
                            progressEl.innerHTML = percent + &#39;%&#39;
                        }
                    }
            }


            //起始位置等于上次上传的结束位置
            ref.sta =  ref.end;
            //结束位置等于上次上传的结束位置+每次上传的字节
            ref.end = ref.sta + ref.blockSize;
        
        }, 5)

    }

    margeUploadFile () {
            console.log("检测上传的文件完整性..........");
            var xhr = new XMLHttpRequest();
            //文件分片的名称/分片大小/总大小
            xhr.open(&#39;GET&#39;, this.margeUrl+ "/" + this.fileSliceName + "/" + this.blockSize + "/" + this.totalize, true);
            xhr.send(); //发送请求
            xhr.onreadystatechange = function () {
                if (xhr.readyState === 4 && xhr.status === 200) {
                    let ret = JSON.parse(xhr.response)
                    if (ret.code == 20000) {
                        console.log("文件上传完毕");
                    } else {
                        console.log("上传完毕但是文件上传过程中出现了异常", ret);
                    }
                }
            }
    
    }

}
export default FileSliceUpload;
ログイン後にコピー

バックエンドJava を使用してファイルのブレークポイント再開機能を実装するにはどうすればよいですか?

コードでは多くの自己カプセル化されたツール クラスが使用されているため、次のコードは次のコードのみを提供します。原則 参考文献

package com.controller.commontools.fIleupload;

import com.alibaba.fastjson.JSON;
import com.application.Result;
import com.container.ArrayByteUtil;
import com.encryption.hash.HashUtil;
import com.file.FileUtils;
import com.file.FileWebUpload;
import com.file.ReadWriteFileUtils;
import com.function.impl.ExecutorUtils;
import com.path.ResourceFileUtil;
import com.string.PatternCommon;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;

@RestController
@RequestMapping("/fileslice")
public class FIleSliceUploadController {

    private  final  String identification="-slice-";
    private  final  String uploadslicedir="uploads"+File.separator+"slice"+File.separator;//分片目录
    private  final  String uploaddir="uploads"+File.separator+"real"+File.separator;//实际文件目录
    //获取分片
    @GetMapping("/testing/{fileName}/{fileSlicSize}/{fileSize}")
    public Result testing(@PathVariable String fileName,@PathVariable long fileSlicSize,@PathVariable long fileSize  ) throws Exception {
        String dir = fileNameMd5Dir(fileName,fileSize);
        String absoluteFilePathAndCreate = ResourceFileUtil.getAbsoluteFilePathAndCreate(uploadslicedir)+File.separator+dir;
        File file = new File(absoluteFilePathAndCreate);
        if (file.exists()) {
            List<String> filesAll = FileUtils.getFilesAll(file.getAbsolutePath());

            if (filesAll.size()<2){
                //分片缺少 删除全部分片文件 ,从新上传
                FileUtils.delFilesAllReview(absoluteFilePathAndCreate,true);
                return Result.Error();
            }

            //从小到大文件进行按照序号排序,和判断分片是否损坏
            List<String> collect = fileSliceIsbadAndSort(file, fileSlicSize);
            //获取最后一个分片
            String fileSliceName = collect.get(collect.size() - 1);
            fileSliceName = new File(fileSliceName).getName();
            int code = fileId(fileSliceName);
            //服务器的分片总大小必须小于或者等于文件的总大小
            if ((code*fileSlicSize)<=fileSize) {
                Result result = new Result();
                String finalFileSliceName = fileSliceName;
                String str = PatternCommon.renderString("{\"code\":\"$[code]\",\"fileSliceName\":\"${fileSliceName}\"}", new HashMap<String, String>() {{
                    put("code", String.valueOf(code));
                    put("fileSliceName", finalFileSliceName);
                }});
                result.setData(JSON.parse(str));
                return result;
            }else {
                //分片异常 ,删除全部分片文件,从新上传
                FileUtils.delFilesAllReview(absoluteFilePathAndCreate,true);
                return Result.Error();
            }
        }
        //不存在
       return Result.Error();
    }


    @PostMapping(value = "/uploads")
    public Result uploads(HttpServletRequest request)  {
        String fileSliceName = request.getParameter("fileSliceName");
        long fileSize = Long.parseLong(request.getParameter("fileSize")); //文件大小
        String dir = fileSliceMd5Dir(fileSliceName,fileSize);
        String absoluteFilePathAndCreate = ResourceFileUtil.getAbsoluteFilePathAndCreate(uploadslicedir+dir);
        FileWebUpload.fileUpload(absoluteFilePathAndCreate,fileSliceName,request);
        int i = fileId(fileSliceName); //返回上传成功的文件id,用于前端计算进度
        Result result=new Result();
        result.setData(i);
        return result;
    }


    // 合并分片
    @GetMapping(value = "/merge-file-slice/{fileSlicNamee}/{fileSlicSize}/{fileSize}")
    public Result mergeFileSlice(@PathVariable String fileSlicNamee,@PathVariable long fileSlicSize,@PathVariable long fileSize ) throws Exception {
        int l =(int) Math.ceil((double) fileSize / fileSlicSize); //有多少个分片
        String dir = fileSliceMd5Dir(fileSlicNamee,fileSize); //分片所在的目录
       String absoluteFilePathAndCreate = ResourceFileUtil.getAbsoluteFilePathAndCreate(uploadslicedir+dir);
        File file=new File(absoluteFilePathAndCreate);
        if (file.exists()){
            List<String> filesAll = FileUtils.getFilesAll(file.getAbsolutePath());

            //阻塞循环判断是否还在上传  ,解决前端进行ajax异步上传的问题
            int beforeSize=filesAll.size();

            while (true){
                 Thread.sleep(1000);
                 //之前分片数量和现在分片数据只差,如果大于1那么就在上传,那么继续
                 filesAll = FileUtils.getFilesAll(file.getAbsolutePath());
                if (filesAll.size()-beforeSize>=1){
                    beforeSize=filesAll.size();
                    //继续检测
                    continue;
                }
                //如果是之前分片和现在的分片相等的,那么在阻塞2秒后检测是否发生变化,如果还没变化那么上传全部完成,可以进行合并了
                //当然这不是绝对的,只能解决网络短暂的波动,因为有可能发生断网很长时间,网络恢复后文件恢复上传, 这个问题是避免不了的,所以我们在下面的代码进行数量的效验
                // 因为我们不可能一直等着他网好,所以如果1~3秒内没有上传新的内容,那么我们默认判定上传完毕
                if (beforeSize==filesAll.size()){
                    Thread.sleep(2000);
                    filesAll = FileUtils.getFilesAll(file.getAbsolutePath());
                    if (beforeSize==filesAll.size()){
                        break;
                    }
                }
            }
            //分片数量效验
            if (filesAll.size()!=l){
                //分片缺少 ,删除全部分片文件,从新上传
                FileUtils.delFilesAllReview(absoluteFilePathAndCreate,true);
                return Result.Error();
            }
            //获取实际的文件名称,组装路径
            String realFileName = realFileName(fileSlicNamee);
            String realFileNamePath = ResourceFileUtil.getAbsoluteFilePathAndCreate(uploaddir+ realFileName);
            //从小到大文件进行按照序号排序 ,和检查分片文件是否有问题
            List<String> collect = fileSliceIsbadAndSort(file, fileSlicSize);
            int fileSliceSize = collect.size();

            List<Future<?>> futures = new ArrayList<>();
            // 将文件按照序号进行合并 ,算出Runtime.getRuntime().availableProcessors()个线程 ,每个线程需要读取多少分片, 和每个线程需要读取多少字节大小
            //有人会说一个分片一个线程不行吗,你想想如果上千或者上万分片的话,你创建这么多的线程需要多少时间,以及线程切换上下文切换和销毁需要多少时间? 
            // 就算使用线程池,也顶不住啊,你内存又有多大,能存下多少队列?,并发高的话直接怼爆
            int availableProcessors = Runtime.getRuntime().availableProcessors();
            //每个线程读取多少文件
            int readFileSize = (int)Math.ceil((double)fileSliceSize / availableProcessors);
            //每个线程需要读取的文件大小
            long readSliceSize = readFileSize * fileSlicSize;
            for (int i = 0; i < availableProcessors; i++) {
                int finalI = i;
                Future<?> future =   ExecutorUtils.createFuture("FIleSliceUploadController",()->{
                    //每个线程需要读取多少字节
                    byte[] bytes=new byte[(int) readSliceSize];
                    int index=0;
                    for (int i1 = finalI *readFileSize,i2 = readFileSize*(finalI+1)>fileSliceSize?fileSliceSize:readFileSize*(finalI+1); i1 < i2; i1++) {
                        try ( RandomAccessFile r = new RandomAccessFile(collect.get(i1), "r");){
                            r.read(bytes, (int)(index*fileSlicSize),(int)fileSlicSize);
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                        index++;
                    }

                    if(finalI==availableProcessors-1){
                        //需要调整数组
                        bytes = ArrayByteUtil.getActualBytes(bytes);
                    }

                    try ( RandomAccessFile w = new RandomAccessFile(realFileNamePath, "rw");){
                        //当前文件写入的位置
                        w.seek(finalI*readSliceSize);
                        w.write(bytes);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                });
                futures.add(future);
            }
            //阻塞到全部线程执行完毕后
            ExecutorUtils.waitComplete(futures);
            //删除全部分片文件
            FileUtils.delFilesAllReview(absoluteFilePathAndCreate,true);
        }else {
            //没有这个分片相关的的目录
            return Result.Error();
        }

        return Result.Ok();



    }






    //获取分片文件的目录
    private String fileSliceMd5Dir(String fileSliceName,long fileSize){
        int i = fileSliceName.indexOf(identification) ;
        String substring = fileSliceName.substring(0, i);
        String dir = HashUtil.md5(substring+fileSize);
        return dir;
    }
    //通过文件名称获取文件目录
    private String fileNameMd5Dir(String fileName,long fileSize){
        return HashUtil.md5(fileName+fileSize);
    }
    //获取分片的实际文件名
    private String realFileName(String fileSliceName){
        int i = fileSliceName.indexOf(identification) ;
        String substring = fileSliceName.substring(0, i);
        return substring;

    }
    //获取文件序号
    private  int fileId(String fileSliceName){
        int i = fileSliceName.indexOf(identification)+identification.length() ;
        String fileId = fileSliceName.substring(i);
        return Integer.parseInt(fileId);
    }



  //判断是否损坏
  private List<String>  fileSliceIsbadAndSort(File file,long fileSlicSize) throws Exception {
        String absolutePath = file.getAbsolutePath();
        List<String> filesAll = FileUtils.getFilesAll(absolutePath);
        if (filesAll.size()<1){
            //分片缺少,删除全部分片文件 ,从新上传
            FileUtils.delFilesAllReview(absolutePath,true);
            throw  new Exception("分片损坏");
        }
        //从小到大文件进行按照序号排序
        List<String> collect = filesAll.stream().sorted((a, b) -> fileId(a) - fileId(b)).collect(Collectors.toList());
        //判断文件是否损坏,将文件排序后,进行前后序号相差大于1那么就代表少分片了
        for (int i = 0; i < collect.size()-1; i++) {
            //检测分片的连续度
            if (fileId(collect.get(i)) - fileId(collect.get(i+1))!=-1) {
                //分片损坏 删除全部分片文件 ,从新上传
                FileUtils.delFilesAllReview(absolutePath,true);
                throw  new Exception("分片损坏");
            }
            //检测分片的完整度
            if (new File(collect.get(i)).length()!=fileSlicSize) {
                //分片损坏 删除全部分片文件 ,从新上传
                FileUtils.delFilesAllReview(absolutePath,true);
                throw  new Exception("分片损坏");
            }
        }
        return  collect;
    }
}
ログイン後にコピー

以上がJava を使用してファイルのブレークポイント再開機能を実装するにはどうすればよいですか?の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

関連ラベル:
ソース:yisu.com
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート
私たちについて 免責事項 Sitemap
PHP中国語ウェブサイト:福祉オンライン PHP トレーニング,PHP 学習者の迅速な成長を支援します!