CODE - WebClient 下載檔案自動取得檔名
2 |
透過 WebClient.DownloadFile() 或 DownloadData() 下載檔案對 .NET 老鳥而言是雕蟲小技(參考:CODE-使用C#程式從網站下載檔案 ),但此種寫法檔名需自行指定。若下載對象非靜態檔案,伺服器端程式會透過 Content-Disposition Response Header 傳回檔名供客戶端參考,WebClient 是否能由 Response Header 自動取得檔名呢?
答案是可以! 程式範例如下:
static string DownloadFile(string url, string saveFolder)
{
using (var wc = new WebClient())
{
using (var stream = wc.OpenRead(url))
{
//若伺服器未提供檔名,預設以下載時間產生檔名
var fn = DateTime.Now.ToString("yyyyMMddHHmmss") + ".data";
var cd = wc.ResponseHeaders["content-disposition"];
if (!string.IsNullOrEmpty(cd))
{
Match m = Regex.Match(cd, "filename[*]=(?<es>[^;]+)");
if (m.Success)
{
fn = DecodeRF5987(m.Groups["es"].Value);
}
else
{
m = Regex.Match(cd, "filename=[\"]*(?<f>[^\";]+)[\"]*");
if (m.Success)
{
fn = m.Groups["f"].Value;
//如伺服器會傳回UrlEncode()格式檔名,視需要加入
if (fn.Contains("%")) fn = Uri.UnescapeDataString(fn);
}
}
}
using (var file = File.Create(Path.Combine(saveFolder, fn)))
{
stream.CopyTo(file);
}
return fn;
}
}
}
//REF: https://github.com/grumpydev/RFC5987-Decoder/blob/master/RFC5987/RFC5987.cs
private static IEnumerable<byte> GetDecodedBytes(string encData)
{
var encChars = encData.ToCharArray();
for (int i = 0; i < encChars.Length; i++)
{
if (encChars[i] == '%')
{
var hexString = new string(encChars, i + 1, 2);
i += 2;
int characterValue;
if (int.TryParse(hexString, NumberStyles.HexNumber,
CultureInfo.InvariantCulture, out characterValue))
{
yield return (byte)characterValue;
}
}
else
{
yield return (byte)encChars[i];
}
}
}
static string DecodeRF5987(string encStr)
{
Match m = Regex.Match(encStr, "^(?<e>.+)'(?<l>.*)'(?<d>[^;]+)$");
if (m.Success)
{
//TODO: 此處未包含伺服器傳回資料有誤之容錯處理
var enc = Encoding.GetEncoding(m.Groups["e"].Value);
return enc.GetString(GetDecodedBytes(m.Groups["d"].Value).ToArray());
}
return encStr;
}
2018-09-17 更新:Content-Disposition Header 可能包含多值(且會帶有雙引號),原本 Regex.Split() 寫法錯抓機率甚高,例如:Content-Disposition: attachement; filename="…"; name="fieldName"; ,更進一步,伺服器端還可能依據 RFC5987,以 filename*=UTF-8''%c2%a3%20and%20rates.pdf 編碼規則提供 Unicode 檔名,故調整以 Regex.Match 取 filename* 或 filename,並加入 RFC5987 解碼。(註: 程式碼未經廣泛驗證,大家如發現有誤請不吝指正)
感謝網友 Slash 提醒。
Demostrating how to use WebClient to download file and get filename from response header.
Comments
# by Slash
雖然與你分享這段小程式的目的不同,但還是要提醒Regex.Split這邊會有RFC5987的問題。 或者應該是說,Content-Disposition可鬆散、多值的表示,故簡單比對套用在無法受控的對方伺服器,儲存到非預期檔案名稱的機率很高。
# by Jeffrey
to Slash, 感謝提醒,程式已修改強化。