書接上文,背景見: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) }
至此,文件分片下載完成。