Node實現斷點續傳

斷點續傳,顧名思義就是文件上傳/下載過程中,遇到不可抗力,比如網絡中斷,服務器異常,或者其他原因導致操作中斷;再次操作時,可以從已經上傳/下載的部分開始繼續上傳/下載未完成的部分,而沒有必要從頭開始上傳/下載。

這樣就避免了文件重複上傳/下載,浪費服務器空間使用,節約服務器資源,而且速度更快,更高效。

斷點續傳-分片上傳

斷點續傳上傳將要上傳的文件分成若干個分片(Part)分別上傳,所有分片都上傳完成後,將所有分片合併成完整的文件,完成整個文件的上傳。

利用MD5 , MD5 是文件的唯一標識,可以利用文件的 MD5 查詢文件的上傳狀態;

設計思路:

將文件切成多個小的文件;

將切片並行上傳;

所有切片上傳完成後,服務器端進行切片合成;

當分片上傳失敗,可以在重新上傳時進行判斷,只上傳上次失敗的部分實現斷點續傳;

當切片合成爲完整的文件,通知客戶端上傳成功;

已經傳到服務器的完整文件,則不需要重新上傳到服務器,實現秒傳功能;

/**
 * 切片上傳
 *
 */
const PATH = require("path");
const FS = require("fs");
const mkDir = require("./ApiMkDir"); // 創建目錄的封裝方法

module.exports = function (req, res, config) {
  return new Promise((resolve, reject) => {
    // 獲取時間
    // 生成儲存目錄名稱
    let date = new Date();
    let path = `${date.getFullYear()}${date.getMonth() + 1}${date.getDate()}`;
    // 文件目錄
    let savePath = PATH.resolve(config.root, "assets/" + path);
    let cachePath = PATH.resolve(config.root, "cacheUploads");
    // 判斷目錄是否存在,不存在創建目錄
    if (!FS.existsSync(savePath)) mkDir(savePath);
    if (!FS.existsSync(cachePath)) mkDir(cachePath);
    // 獲取分片數據
    let { index, total, md5 } = req.fields;
    // 臨時文件路徑
    let TmpFileName = cachePath + "/" + req.files["file"].name;
    // 存儲文件路徑
    let FileName = savePath + "/" + req.files["file"].name;
    // 當前文件傳輸進度管理
    let TmpFileNameMange = cachePath + "/" + req.files["file"].name + ".txt";
    TmpFileName = PATH.normalize(TmpFileName);
    TmpFileNameMange = PATH.normalize(TmpFileNameMange);
    FileName = PATH.normalize(FileName);
    // 是否第一次傳輸
    if (FS.existsSync(FileName)) FS.unlinkSync(FileName);
    if (FS.existsSync(TmpFileNameMange)) {
      let test = FS.readFileSync(TmpFileNameMange, { encoding: "utf-8" });
      if (test) {
        let { i, md5: md } = JSON.parse(test);
        if (md === md5 && Number(i) > Number(index)) {
          resolve(i);
          return;
        }
      }
    }
    // 獲取上傳的文件buffer
    let buffer = FS.readFileSync(req.files["file"].path);
    // 寫入臨時文件
    if (FS.appendFileSync(TmpFileName, buffer)) reject();
    // 傳輸完成,移動到保存目錄
    // 寫入保存文件
    FS.writeFileSync(
      TmpFileNameMange,
      JSON.stringify({ i: index, total, md5 })
    );
    if (index === total) {
      FS.renameSync(TmpFileName, FileName);
      FS.unlinkSync(TmpFileNameMange);
      resolve();
      return;
    }
    resolve(index);
  });
};
 async submit() {
      let file = this.$refs.file.files[0];
      this.upload(file);
    },
    async upload(file, index = 0) {
      // 獲取文件大小
      let fileSize = file.size;
      // 每個塊的大小
      let chunkSize = 1024 * 1024 * 0.0005;
      // 共多少塊
      let chunkNum = Math.ceil(fileSize / chunkSize);
      // 定義formData對象
      let formData = new FormData();
      // 定義結束位置;
      let end = index + 1;
      // 片段是否最後一片,如果不是最後一片,那麼就是每片的位置
      if (end < chunkNum) end = end * chunkSize;
      // 如果是最後一片,結束位置等於文件最後的位置
      else end = fileSize;
      // 獲取單個切片
      let chunData = file.slice(index * chunkSize, end);
      // 儲存單個切片
      formData.append("file", chunData, file.name);
      formData.append("index", index + 1); //第幾片
      formData.append("total", chunkNum); //第幾片
      formData.append('md5',md5(file))
      let { data } = await axios({
        url: "http://127.0.0.1:3000/api/chunkUpload",
        method: "post",
        data: formData,
        headers: { token: "token" }
      });
      // 後臺需要返回當前切片位置
      index = data.data - 0; 
      if (end < fileSize) this.upload(file, index);
    }

斷點續傳-下載

要實現斷點續傳,需要進行以下步驟:獲取文件的大小和已經下載的部分。

在下載文件之前,需要確定要下載的文件的大小以及已經下載的部分。

可以使用 Node.js 中的 fs.statSync() 方法獲取文件的大小,或者通過發起 HEAD 請求來獲取文件的大小。

對於已經下載的部分,需要記錄在先前下載時下載的字節數。

使用 HTTP Range 頭。 HTTP Range 頭允許客戶端請求服務器僅發送文件的某一部分。

使用 Range 頭可以讓我們下載文件的指定部分,從而實現斷點續傳。

我們可以使用 HTTP 模塊中的 request() 方法來發送 HTTP 請求,並在請求頭中包含 Range 頭。

將下載的部分寫入文件,可以使用 Node.js 中的 fs.createWriteStream() 方法創建一個寫入文件的流,並在下載過程中將數據寫入文件。

設計思路:

第一步:檢查文件是否存在,如果存在,就設置 Range 頭,這樣服務器就只會返回文件的未下載部分。

第二步:創建一個寫入文件的流,併發送 HTTP 請求。

第三步:如果服務器返回狀態碼 206(Partial Content),則表示可以進行斷點續傳;否則,就從頭開始下載整個文件。

第四步:最後,將響應流寫入文件。

代碼示例:

const fs = require('fs');
const http = require('http');

const fileUrl = 'http://example.com/file.zip';
const filePath = './file.zip';

// 獲取文件大小和已下載的部分
let options = {};
if (fs.existsSync(filePath)) {
  const stat = fs.statSync(filePath);
  const range = `bytes=${stat.size}-`;
  options.headers = { 'Range': range };
}

// 發送請求並寫入文件
const file = fs.createWriteStream(filePath, { flags: 'a' });
const request = http.get(fileUrl, options, (response) => {
  // 如果服務器返回 206(Partial Content),則表示可以斷點續傳
  if (response.statusCode === 206) {
    console.log('繼續下載');
  } else if (response.statusCode === 200) {
    console.log('從頭開始下載');
  } else {
    console.log(`無法下載,錯誤代碼:${response.statusCode}`);
    return;
  }
  response.pipe(file);
});

// 處理錯誤
request.on('error', (err) => {
  console.error(`下載出錯:${err.message}`);
});

// 下載完成
file.on('finish', () => {
  console.log('下載完成');
});

// 關閉文件流
file.on('close', () => {
  console.log('文件流已關閉');
});

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章