1. 程式人生 > >用Electron開發企業網盤(二)--分片下載

用Electron開發企業網盤(二)--分片下載

  書接上文,背景見:https://www.cnblogs.com/shawnyung/p/10060119.html

HTTP請求頭  Range

  請求資源的部分內容(不包括響應頭的大小),單位是byte,即位元組,從0開始。

  如果伺服器能夠正常響應的話,伺服器會返回 206 Partial Content 的狀態碼及說明.

  如果不能處理這種Range的話,就會返回整個資源以及響應狀態碼為 200 OK 。

Range請求頭格式

Range: bytes=start-end

響應頭

Conent-Length

  表示這次伺服器響應資料的位元組數

一、思路整理

  用過迅雷等下載工具會發現:檔案在下載過程中,會生成.downloading字尾和.downloading.cfg字尾的兩個檔案。.downloading字尾的檔案跟檔案已下載的大小是一致的,而.downloading.cfg字尾的檔案特別小。當檔案下載完成後,.downloading字尾及.downloading.cfg檔案均不存在,只保留下載完成的檔案。

  通過網上了解知道,cfg檔案大多是配置檔案。那麼可以 推測出:.downloading檔案是下載的臨時檔案,接收下載檔案流。而.downloading.cfg是下載的配置檔案,儲存檔案下載的相關資訊。

  配合斷點續傳的需求,梳理出分片下載的方案:檔案下載,首先判斷當前目錄有沒有已下載的斷點檔案。若有,則建立一個'append'的檔案流,通過.downloading.cfg檔案讀取已下載分片的相關資訊,續傳下載;若無,則建立一個新檔案流,指定請求檔案的部分內容(分片)。傳輸過程中,將檔案流寫入.downloading檔案,並同步更新.downloading.cfg檔案,記錄下載檔案的相關資訊及分片資訊。每一片傳輸完成,判斷伺服器相應資料的位元組是否小於分片位元組數。若是,表示為最後一個分片,檔案已下載完成,將.downloading檔案重新命名為原檔名並刪除.downloading.cfg檔案。

二、分解任務

  將任務分解成幾個子任務:

1、遞迴建立資料夾。

2、判斷當前目錄有沒有已下載的斷點檔案,建立檔案流。

3、設定HTTP請求頭Range,分片請求檔案url。

 4、更新.downloading.cfg檔案。

 5、檔案下載完成,重新命名.downloading檔案並刪除.downloading.cfg檔案。

1、遞迴建立資料夾

  完整路徑為“D:/tmp/新建資料夾/002.docx”之類的檔案在下載時需要先一級一級建立資料夾。藉助Node的fs及path模組,完成遞迴建立資料夾任務。

const fs = require("fs")
const path = require("path")

const mkdirs = (dirname, callback, errback) => {
  fs.stat(dirname, (err, stats) => {
    if (err) {
      mkdirs(path.dirname(dirname), () => {
        fs.mkdir(dirname, callback)
      }, errback)
    } else {
        if (stats.isDirectory()) {
            callback()
        } else {
              errback()
        }
    }
  })
}

2、父級資料夾建立好後,判斷當前目錄有沒有已下載的斷點檔案,建立檔案流。

  fs.createWriteStream返回WriteSteam物件,用於建立檔案寫入流。

fs.createWriteStream(path[, options])

還是藉助Node的fs模組的stat方法,檢測當前目錄有沒有.downloading檔案。若有,則建立一個flags為'a'的檔案流;若無,則建立一個預設的檔案流。

let statDir = function (flag) {
    fs.stat(file.path + '.downloading', function (err, stats) {
      if (flag) {
        if (err) {
          contents.send('download-error', file.path)
          stream.end()
          return
        }
      } else {
        stream = !err ? fs.createWriteStream(file.path + '.downloading', {flags: 'a'}) :
        fs.createWriteStream(file.path + '.downloading')
        streams.push(stream)
        if (!err) {
          receivedBytes += stats.size
        }
      }
      func()
    })
  }

3、設定HTTP請求頭Range,分片請求檔案url。

net

使用Chromium的原生網路庫發出HTTP / HTTPS請求

  net 模組是一個傳送 HTTP(S) 請求的客戶端API。 它類似於Node.js的HTTP 和 HTTPS 模組 ,但它使用的是Chromium原生網路庫來替代Node.js的實現,提供更好的網路代理支援。

  receivedBytes為.downloading臨時檔案已下載的檔案流大小,chunkSize為分片大小。所以每個分片的請求內容為receivedBytes至receivedBytes + chunkSize - 1。每個分片下載完成後,更新receivedBytes大小。

const request = net.request(url)
    let start = receivedBytes
    let end = receivedBytes + chunkSize - 1
    request.setHeader('Range', 'bytes=' + start + '-' + end)
    request.on('response', (response) => {
      response.on('data', chunk => {
        if (response.statusCode == 206) {
          try {
            stream.write(chunk)
          } catch(e) {}
        }
      })

      let contentLength = response.headers['content-length'][0]
      response.on('end', () => {
        receivedBytes += parseInt(contentLength)
    }

4、更新.downloading.cfg檔案,記錄下載檔案的相關資訊及分片資訊。

  .downloading檔案儲存檔案的進度,大小,路徑等資訊。用於啟動應用時,讀取並渲染續傳列表,顯示檔名,檔案大小,進度條等資訊。

let json = {
          percent: percent,
          filesize: file.filesize,
          md5: file.md5,
          uid: file.uid ,
          bucketName: file.bucketName,
          path: file.path,
        }
        try {
          !stream.closed && fs.writeFileSync(file.path + '.downloading.cfg', JSON.stringify(json))
        } catch(e) {}

5、最後一個分片下載完成 ,將.downloading檔案重新命名為原檔名並刪除.downloading.cfg檔案。

getList

獲取當前目錄下的檔案列表。

  獲取檔案列表後,算出重新命名後的檔名(如果當前目錄有重名檔案,則需要將檔案重新命名。重新命名演算法見系列文章(一))。將.downloading檔案重新命名為算出的檔名並刪除.downloading.cfg檔案。

if (contentLength < chunkSize) {
          stream.end()
          endStream(file.path)
          try {
            getList(dirname).then(fileList => {
              let newName = fileRename(fileList, filename, 'filename')
              setTimeout(() => {
                fs.rename(file.path + '.downloading', path.join(dirname, newName), (err) => {
                  if (err) {
                    return console.error(err)
                  }
                })
              }, 500)
            })

            fs.unlink(file.path + '.downloading.cfg', function (er) {
              if (er) {
                 return console.error(er);
               }
            })
          } catch (e) { console.log(e) }
        } else {
          !stream.closed && statDir(true)
        }

  至此,檔案分片下載完成。