從網站下載大檔案,若下載一半中斷,從中斷處繼續下載已是所有瀏覽器的基本功能。其背後原理是透過 HTTP 1.1 協定加入的 HTTP Accept-Ranges 及 Range 規格,網站透過 Response Header Accept-Ranges: byte 表明自己接受分段下載;客戶端發送 GET Request 時加入 Header Range: bytes=2048-150000 指定下載範圍,從第幾個 Byte 下載到第幾個 Byte。主流網站如 IIS、Apache 早已支援 Range 續傳功能,而 .NET 從 1.1 時代也提供 HttpWebRequest.AddRange()方法實現續傳。看到這裡,想必大家知道我要做什麼了,是的,我想試試自己寫 C#/PowerShell 程式實現下載中斷續傳。

對自己造輪子沒興趣的朋友,以下是一些支援續傳下傳的現成解決方案:

讀到這裡的同學,歡迎加入造輪子的行列。

為求省事,我用 PowerShell 寫,但核心部分是靠 C# HttpWebRequest,相同邏輯可輕易轉成 .NET 程式。不囉嗦,直接上程式碼:

$ErrorActionPreference = "STOP"
$url = "http://localhost/aspnet/music.mp3"
$file = "test.mp3"
if (!(Test-Path $file)) {
    # 若檔案不存在,用單純 Invoke-WebRequest 或 WebClient.DownloadFile 就好
    # (New-Object System.Net.WebClient).DownloadFile($url, $file)
    Invoke-WebRequest -Uri $url -OutFile $file
}
else {
    # .NET 工作目錄與 PowerShell 可能不用,取得完整路徑供 .NET 使用
    $fileFullPath = (Resolve-Path $file).Path    
    # 若檔案存在,查現有檔案大小,使用 Range Header 續傳
    # 取得現有檔案大小,由後面續傳
    # PowerShell 7 Invoke-WebRequest 直接加 -Resume 即可
    $currLength = (Get-Item $file).Length
    # Invoke-WebRequest -Uri $url -Headers @{"Range"="bytes=$currLength-"} -OutFile "$file.resume"
    # 以上寫法不 Work -> The 'RANGE' header must be modified using the appropriate property or method.
    # 用 HttpWebRequest 實現
    [System.Net.HttpWebRequest] $req = [System.Net.WebRequest]::Create($url)
    $req.Method = "GET"
    $req.AddRange($currLength)
    try {
        $resp = $req.GetResponse()
    }
    catch [System.Net.WebException] {
        # 若檔案先前已下載完成,伺服器會由 Range 已到檔案結尾回傳 HTTP 416,此時不需續傳,直接結束
        if ($_.Exception.Response.StatusCode -eq [System.Net.HttpStatusCode]::RequestedRangeNotSatisfiable) {
            Write-Host "檔案已完成"
            return
        }
        else {
            $_.Exception
        }
    }
    Write-Host "從 $currLength 開始續傳"
    $respStream = $resp.GetResponseStream()
    $contRange = $resp.Headers['Content-Range'] # ex: Content-Range: bytes 0-50/1270
    if (!$contRange) { throw "無法續傳" }
    $totalLen = $contRange.Split('/')[1]
    $fileStream = New-Object System.IO.FileStream($fileFullPath, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Write, [System.IO.FileShare]::Read)
    try {
        $fileStream.Seek($currLength, [System.IO.SeekOrigin]::Begin) | Out-Null
        # 以 8K 為單位從 Response Stream 讀取 byte[] 寫入 FileStream
        [byte[]]$buff = [byte[]]::CreateInstance([byte], 8192)
        do {
            $bytesRead = $respStream.Read($buff, 0, $buff.Length)
            $fileStream.Write($buff, 0, $bytesRead)
            Write-Progress -Activity "續傳下載中" -Status "$($fileStream.Position)/$totalLen" -PercentComplete ($fileStream.Position * 100 / $totalLen)
        } while ($bytesRead -gt 0)
        $fileStream.Close()
    }
    finally {
        $fileStream.Dispose()
    }
    $respStream.Close()
}

簡單測試,第一次下載到一半按 Ctrl-C 中斷,再次執行時會從上次中斷的地方繼續,最後我用 FC.exe 工具檢查,確認下載內容與原始檔案完全一致。

加碼測試多次續傳,也 OK。

就醬,我們現在也會用 .NET 寫 HTTP 續傳下載功能了。

Example of how to resume file download with HttpWebRequest in C#/PowrShell.


Comments

Be the first to post a comment

Post a comment