1. 程式人生 > 其它 >Javaweb分片上傳大檔案

Javaweb分片上傳大檔案

Javaweb分片上傳大檔案

大檔案上傳採取分片上傳,實現為:1、分片上傳檔案,2、合併檔案
1、後端java介面程式碼
 /**
 * 上傳分片檔案
 * @param file 分片檔案
 * @param fileid 前端生成的uuid,用於指定此次上傳的唯一標識
 * @param request
 * @return
 * @throws IOException
 */
@RequestMapping(value = "/file/uploadFilePart", method = {RequestMethod.POST})
public Map<String,Object> uploadFilePart(@RequestParam("file") MultipartFile file,String fileid,HttpServletRequest request) throws IOException {
    Map<String,Object> resultMap = new HashMap<>();

    String basePath =  appConfig.getZipUnPath() + File.separatorChar + fileid;
    File baseDir= new File(basePath);
    if(!baseDir.exists()) {
        baseDir.mkdirs();
    }
    System.out.println("檔案目錄為:"+ basePath);

    Integer current = Integer.parseInt(request.getParameter("index"));
    // 總分片數
    Integer total = Integer.parseInt(request.getParameter("total"));

    // 當前檔案的路徑
    String fileNamePath = basePath + File.separatorChar + current;
    OutputStream out = new FileOutputStream(fileNamePath);
    // 獲取檔案的相關資料,然後寫入到檔案中
    byte[] bytes = file.getBytes();
    out.write(bytes);
    out.flush();
    out.close();

    return resultMap;
}

 /**
 * 合併分片檔案
 * @param filename 合併後的檔名(也可後端自定義)
 * @param fileid 前端生成的uuid,用於指定此次上傳的唯一標識
 * @return
 * @throws IOException
 */
@RequestMapping(value = "/file/mergeFilePart", method = {RequestMethod.POST})
public Map<String,Object> mergeFilePart(String filename,String fileid) throws IOException {
    Map<String,Object> resultMap = new HashMap<>();

    String basePath =  appConfig.getZipUnPath() + File.separatorChar + fileid;
    int combineFlag = 0;
    // 合併操作
    try {
        String fileUrl = combineFiles(filename, basePath);
        if(StringUtils.isNotBlank(fileUrl)) {
            combineFlag = 1;
            resultMap.put("filePath", fileUrl);
        }
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        if(combineFlag == 0) {
            // 合併失敗
            resultMap.put("errMessage", "合併檔案失敗");
            resultMap.put("code", 500);
        } else {
            resultMap.put("code", 0);
        }
    }
    resultMap.put("code", 0);
    resultMap.put("massage", "合併成功");
    return resultMap;
}

//合併檔案
private String combineFiles(String fileName, String basePath) throws Exception {
    int returnFlag = CombineFile(fileName, basePath);
    String fileUrl = null;
    if(returnFlag == 1) {
        // 此處表示檔案合併成功,合併後的檔案路徑為:basePath+"/"+fileName
        fileUrl = basePath+"/"+fileName;
    }
    return fileUrl;
}

/**
 * 合併檔案,
 * @param tar 合成目標的檔名稱
 * @param baseFilePath 你要合併哪個資料夾的檔案,裡面必須是要合併的檔案
 * @return
 * @throws Exception
 */
public int CombineFile(String tar, String baseFilePath) throws Exception {
    File dirFile = new File(baseFilePath);
    FileInputStream fis;

    // 一次讀取2M資料,將讀取到的資料儲存到byte位元組陣列中
    byte buffer[] = new byte[1024 * 1024 * 2];
    int len;
    // 判斷file是否為目錄
    if(dirFile.isDirectory()) {
        String[] fileNames = dirFile.list();
        FileOutputStream fos = new FileOutputStream(baseFilePath + File.separatorChar + tar);
        // 實現目錄自定義排序
        Arrays.sort(fileNames, new StringComparator());
        for (int i = 0;i<fileNames.length ;i++){
            fis = new FileInputStream(baseFilePath + File.separatorChar + fileNames[i]);
            len = 0;
            while ((len = fis.read(buffer)) != -1) {
                // buffer從指定位元組陣列寫入。buffer:資料中的起始偏移量,len:寫入的字數。
                fos.write(buffer, 0, len);
            }
            fos.flush();
            fis.close();
        }
        fos.close();
    }
    return 1;
}
檔名比較類

import java.util.Comparator;

/**
 * 檔名排序比較類
 */
public class StringComparator implements Comparator<String> {
    @Override
    public int compare(String s1, String s2) {
        if (returnDouble(s1) < returnDouble(s2)){
            return -1;
        } else if (returnDouble(s1) > returnDouble(s2)) {
            return 1;
        } else {
            return 0;
        }
    }

    public static double returnDouble(String str){
        StringBuffer sb = new StringBuffer();
        for(int i=0;i<str.length();i++){
            if(Character.isDigit(str.charAt(i))) {
                sb.append(str.charAt(i));
            }  else if(str.charAt(i)=='.'&&i<str.length()-1&&Character.isDigit(str.charAt(i+1))) {
                sb.append(str.charAt(i));
            } else {
                break;
            }
        }
        if(sb.toString().isEmpty()){
            return 0;
        } else {
            return Double.parseDouble(sb.toString());
        }

    }
}


2、前端程式碼比較簡單,網上尋找根據自己需要更改的,簡單的js+html實現的分片上傳檔案

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

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>檔案上傳</title>
    <script src="https://cdn.bootcss.com/axios/0.18.0/axios.min.js"></script>
    <script src="https://code.jquery.com/jquery-3.4.1.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/spark-md5/3.0.0/spark-md5.js"></script>
    <style>
        /* 自定義進度條樣式 */
        .precent input[type=range] {
            -webkit-appearance: none;
            /*清除系統預設樣式*/
            width: 7.8rem;
            /* background: -webkit-linear-gradient(#ddd, #ddd) no-repeat, #ddd; */
            /*設定左邊顏色為#61bd12,右邊顏色為#ddd*/
            background-size: 75% 100%;
            /*設定左右寬度比例*/
            height: 0.6rem;
            /*橫條的高度*/
            border-radius: 0.4rem;
            border: 1px solid #ddd;
            box-shadow: 0 0 10px rgba(0,0,0,.125) inset ;
        }

        /*拖動塊的樣式*/
        .precent input[type=range]::-webkit-slider-thumb {
            -webkit-appearance: none;
            /*清除系統預設樣式*/
            height: .9rem;
            /*拖動塊高度*/
            width: .9rem;
            /*拖動塊寬度*/
            background: #fff;
            /*拖動塊背景*/
            border-radius: 50%;
            /*外觀設定為圓形*/
            border: solid 1px #ddd;
            /*設定邊框*/
        }

    </style>
</head>

<body>
<h1>大檔案分片上傳測試</h1>
<div>
    <input id="file" type="file" name="avatar" />
    <div style="padding: 10px 0;">
        <input id="submitBtn" type="button" value="提交" />
        <input id="pauseBtn" type="button" value="暫停" />
    </div>
    <div class="precent">
        <input type="range" value="0" /><span id="precentVal">0%</span>
    </div>
</div>
<script type="text/javascript" src="./index.js"></script>
</body>

</html>

$(document).ready(() => {
    const submitBtn = $('#submitBtn');  //提交按鈕
    const precentDom = $(".precent input")[0]; // 進度條
    const precentVal = $("#precentVal");  // 進度條值對應dom
    const pauseBtn = $('#pauseBtn');  // 暫停按鈕
    // 每個chunk的大小,設定為1兆
    const chunkSize = 1 * 1024 * 1024;
    // 獲取slice方法,做相容處理
    const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
    // 對檔案進行MD5加密(檔案內容+檔案標題形式)
    const hashFile = (file) => {
        return new Promise((resolve, reject) => {
            const chunks = Math.ceil(file.size / chunkSize);
            let currentChunk = 0;
            const spark = new SparkMD5.ArrayBuffer();
            const fileReader = new FileReader();
            function loadNext() {
                const start = currentChunk * chunkSize;
                const end = start + chunkSize >= file.size ? file.size : start + chunkSize;
                fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
            }
            fileReader.onload = e => {
                spark.append(e.target.result); // Append array buffer
                currentChunk += 1;
                if (currentChunk < chunks) {
                    loadNext();
                } else {
                    console.log('finished loading');
                    const result = spark.end();
                    // 通過內容和檔名稱進行md5加密
                    const sparkMd5 = new SparkMD5();
                    sparkMd5.append(result);
                    sparkMd5.append(file.name);
                    const hexHash = sparkMd5.end();
                    resolve(hexHash);
                }
            };
            fileReader.onerror = () => {
                console.warn('檔案讀取失敗!');
            };
            loadNext();
        }).catch(err => {
            console.log(err);
        });
    }

    // 提交
    submitBtn.on('click', async () => {
        var pauseStatus = false;
        var nowUploadNums = 0
        // 1.讀取檔案
        const fileDom = $('#file')[0];
        const files = fileDom.files;
        const file = files[0];
        if (!file) {
            alert('沒有獲取檔案');
            return;
        }
        // 2.設定分片引數屬性、獲取檔案MD5值
        const hash = await hashFile(file); //檔案 hash
        const blockCount = Math.ceil(file.size / chunkSize); // 分片總數
        const axiosPromiseArray = []; // axiosPromise陣列
        const fileid = getUuid();
        const naame = file.name;
        debugger;
        // 檔案上傳
        const uploadFile = () => {
            const start = nowUploadNums * chunkSize;
            const end = Math.min(file.size, start + chunkSize);
            // 構建表單
            const form = new FormData();
            // blobSlice.call(file, start, end)方法是用於進行檔案分片
            form.append('file', blobSlice.call(file, start, end));
            form.append('index', nowUploadNums);
            form.append('total', blockCount);
            //form.append('hash', hash);
            form.append('filename', name);
            form.append('fileid', fileid);
            // ajax提交 分片,此時 content-type 為 multipart/form-data
            const axiosOptions = {
                onUploadProgress: e => {
                    nowUploadNums++;
                    // 判斷分片是否上傳完成
                    if (nowUploadNums < blockCount) {
                        setPrecent(nowUploadNums, blockCount);
                        uploadFile(nowUploadNums)
                    } else {
                        // 4.所有分片上傳後,請求合併分片檔案
                        setPrecent(blockCount, blockCount); // 全部上傳完成
                        var merge = new FormData();
                        debugger;
                        merge.append('filename', file.name);
                        merge.append('total', blockCount);
                        merge.append('fileid', fileid);
                        axios.post('/file/mergeFilePart',merge).then(res => {
                            console.log(res.data, file);
                            pauseStatus = false;
                            alert('上傳成功');
                        }).catch(err => {
                            console.log(err);
                        });
                    }
                },
            };
            // 加入到 Promise 陣列中
            if (!pauseStatus) {
                axiosPromiseArray.push(axios.post('/file/uploadFilePart', form, axiosOptions));
            }

        }
        function getUuid() {
            return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
                var r = (Math.random() * 16) | 0,
                    v = c == 'x' ? r : (r & 0x3) | 0x8;
                return v.toString(16);
            });
        }
        // 設定進度條
        function setPrecent(now, total) {
            var prencentValue = ((now / total) * 100).toFixed(2)
            precentDom.value = prencentValue
            precentVal.text(prencentValue + '%')
            precentDom.style.cssText = `background:-webkit-linear-gradient(top, #059CFA, #059CFA) 0% 0% / ${prencentValue}% 100% no-repeat`
        }
        // 暫停
        pauseBtn.on('click', (e) => {
            pauseStatus = !pauseStatus;
            e.currentTarget.value = pauseStatus ? '開始' : '暫停'
            if (!pauseStatus) {
                uploadFile(nowUploadNums)
            }
        })
        uploadFile();
    });
})

3、效果展示,壓縮檔案為合併後的檔案