用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])
path
<string> | <Buffer> | <URL>- Returns: <fs.WriteStream> See Writable Stream.
還是藉助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) }
至此,檔案分片下載完成。