using System;
using System.Diagnostics;
using System.Net;
using System.Net.Mime;
using System.IO;
using System.Threading;
using NaGet.Tasks;
namespace NaGet.SubCommands.SubTask
{
///
/// ダウンロードタスク
///
public class DownloadSubTask : NaGetSubTask
{
///
/// アクセスURL
///
protected Uri url;
///
/// プロキシ
///
protected IWebProxy proxy;
///
/// 保存先
///
protected string filepath;
///
/// リクエストオブジェクト
///
protected WebRequest request;
///
/// レスポンスオブジェクト。応答がくるまではnullである。
///
protected WebResponse response;
///
/// ダウンロード時にHTTPヘッダなどから取得した本来のファイル名
///
private string downloadedFileName = null;
///
/// ダウンロード要求時のキャッシュレベル。デフォルトではキャッシュ無視
///
public System.Net.Cache.RequestCacheLevel CacheLevel = System.Net.Cache.RequestCacheLevel.NoCacheNoStore;
///
/// ダウンロード時に downloadedFileName に改名するか否か。
///
protected bool enableChangeFileName = false;
///
/// キャンセルが呼ばれたか否か。
///
private bool cancelCalled = false;
///
/// ダウンロード中のファイル名につける接尾辞
///
private static readonly string PartialFilePostfix = ".part";
///
/// コンストラクタ
///
/// ダウンロード先URL
/// 保存ファイルパス
/// プロキシ
public DownloadSubTask(Uri url, string filepath, IWebProxy proxy)
{
this.url = url;
this.filepath = filepath;
this.proxy = proxy;
this.request = null;
this.response = null;
this.downloadedFileName = null;
}
public DownloadSubTask(string url, string filepath, IWebProxy proxy)
: this(new Uri(url), filepath, proxy)
{
}
///
/// コンストラクタ
///
/// ダウンロード先URL
/// 保存ファイルパス
public DownloadSubTask(Uri url, string filepath)
: this(url, filepath, NaGet.Env.WebProxy)
{
}
public DownloadSubTask(string url, string filepath)
: this(new Uri(url), filepath, NaGet.Env.WebProxy)
{
}
///
/// ダウンロード時にHTTPヘッダなどから取得した本来のファイル名
///
public string DownloadedFileName {
get { return downloadedFileName; }
}
///
/// 保存先ファイル名を本来のファイル名に変えるか。
///
public bool EnableChangeFileName {
get { return enableChangeFileName; }
set { enableChangeFileName = value; }
}
///
/// 保存ファイル。
///
public string Filepath {
get { return filepath; }
}
///
/// キャンセル可能
///
public override bool Cancelable {
get { return !this.cancelCalled; }
}
///
/// ダウンロード処理をキャンセルする
///
/// キャンセルに成功したときtrue
public override bool Cancel()
{
if (! this.cancelCalled && ! this.Done) {
this.cancelCalled = true;
if (request != null) {
try {
request.Abort();
} catch (WebException) {
}
}
return true;
} else {
return false;
}
}
public override void Run()
{
NotifyStarted();
RaiseTaskSetEvent(TaskEventType.STARTED, string.Format("ダウンロード:{0}", this.url), 0);
try {
runBuildRequest();
runHandleCancelTrigger();
RaiseTaskSetEvent(TaskEventType.PING, string.Format("接続中...{0}", this.url.Host), -1);
runAcquireResponse();
runHandleCancelTrigger();
runPrepareFile();
runDownloadToFile();
runHandleCancelTrigger();
runPostprocess();
RaiseTaskSetEvent(TaskEventType.COMPLETED, "ダウンロード終了", 100);
} catch (System.Net.WebException e) {
if (System.Net.NetworkInformation.NetworkInterface.GetIsNetworkAvailable()) {
RaiseTaskSetEvent(TaskEventType.WARNING, "ネットワークに接続されていません。", -1);
} else {
RaiseTaskSetEvent(TaskEventType.WARNING, "ネットワークに接続できませんでした。ネットワークが切断されているか、ファイアウォールによって遮断された可能性があります。", -1);
}
throw new System.Net.WebException(e.Message, e);
} finally {
runReleaseResponse();
if (cancelCalled) {
NotifyCancelled();
} else {
NotifyCompleted();
}
}
}
///
/// キャンセルされたかを確認して、キャンセル要求があるのならば TaskCanceledException を投げる
///
private void runHandleCancelTrigger()
{
if (this.cancelCalled) {
throw new TaskCanceledException(string.Empty);
}
}
///
/// requestの構築。
///
private void runBuildRequest()
{
request = WebRequest.Create(url);
request.Proxy = this.proxy;
request.CachePolicy = new System.Net.Cache.RequestCachePolicy(CacheLevel);
HttpWebRequest httpRequest = request as HttpWebRequest;
if (httpRequest != null) {
httpRequest.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip;
httpRequest.UserAgent = NaGet.Env.UserAgentString;
}
}
///
/// Responseを得る
///
private void runAcquireResponse()
{
try {
response = request.GetResponse();
} catch (WebException e) {
if (cancelCalled) { // キャンセル時
throw new TaskCanceledException(string.Empty, e);
} else {
throw new WebException(e.Message, e);
}
}
}
///
/// ダウンロード先ファイル名の決定
///
private void runPrepareFile()
{
try {
downloadedFileName = getFileNameFromWebResponse(response);
} catch (Exception) {
}
// パス名を変えるときは、HTTPヘッダから取得したファイル名に変更する。
if (enableChangeFileName && (!string.IsNullOrEmpty(downloadedFileName))) {
filepath = Path.Combine(Path.GetDirectoryName(filepath), downloadedFileName);
}
// ファイルが存在するとき削除
if (File.Exists(filepath)) {
File.Delete(filepath);
}
// 部分ファイルが存在するときも削除 TODO レジューム処理
if (File.Exists(filepath + PartialFilePostfix)) {
File.Delete(filepath + PartialFilePostfix);
}
}
private void runDownloadToFile()
{
Stopwatch stopwatch = new Stopwatch();
string partialFilepath = filepath + PartialFilePostfix;
using (Stream stream = response.GetResponseStream() )
using (FileStream fs = new FileStream(partialFilepath,
FileMode.Create,
FileAccess.Write) ) {
try {
File.SetAttributes(partialFilepath, FileAttributes.Hidden);
long contentLength = response.ContentLength;
stopwatch.Start();
RaiseDownloadProgressTaskSetEvent(0, contentLength, 0);
Timer timer = new Timer(new TimerCallback(
delegate(object obj) {
try {
RaiseDownloadProgressTaskSetEvent(fs.Position, contentLength, stopwatch.ElapsedMilliseconds);
} catch (ObjectDisposedException) {
}
}), null, 0, 1000);
try {
byte[] data = new byte[4096];
int size = 0;
while ((size = stream.Read(data,0,data.Length)) > 0) {
fs.Write(data, 0, size);
if (cancelCalled) {
throw new TaskCanceledException(string.Empty);
}
}
} finally {
timer.Dispose();
}
} catch (IOException ex) {
if (cancelCalled) {
throw new TaskCanceledException(string.Empty);
} else {
throw new IOException(ex.Message, ex);
}
} finally {
if (stopwatch != null) {
stopwatch.Stop();
stopwatch = null;
}
}
}
if (File.Exists(partialFilepath)) {
File.Move(partialFilepath, filepath);
File.SetAttributes(filepath, FileAttributes.Normal);
}
}
private void runPostprocess()
{
// 更新日を補完
if (File.Exists(filepath)) {
HttpWebResponse httpResponse = response as HttpWebResponse;
FtpWebResponse ftpResponse = response as FtpWebResponse;
if (httpResponse != null) {
File.SetLastWriteTime(filepath, httpResponse.LastModified);
} else if (ftpResponse != null) {
File.SetLastWriteTime(filepath, ftpResponse.LastModified);
}
}
}
///
/// responseの開放
///
private void runReleaseResponse()
{
if (response != null) {
response.Close();
}
}
///
/// Webレスポンスからダウンロードしたファイルの名前を取得
///
/// Content-Dispositionヘッダから取得あるいはURLの末尾から推定します
/// レスポンスオブジェクト
/// 取得したファイル名
private static string getFileNameFromWebResponse(WebResponse response)
{
HttpWebResponse httpresp = response as HttpWebResponse;
if (httpresp != null) {
string contentDisposition = httpresp.Headers["Content-Disposition"];
if (! string.IsNullOrEmpty(contentDisposition)) {
// おかしな Content-Disposition ヘッダ向け
//(Content-Disposition: filename=\"...\" に attachment; を補って RFC1806 に準拠させる)
if (System.Text.RegularExpressions.Regex.IsMatch(contentDisposition, @" *filename=", System.Text.RegularExpressions.RegexOptions.IgnoreCase)) {
contentDisposition = "attachment; " + contentDisposition;
}
// "atachment;filename=\""のようにセミコロンの後ろにスペースがない場合、それを補充する
if (System.Text.RegularExpressions.Regex.IsMatch(contentDisposition, @";[^ ]", System.Text.RegularExpressions.RegexOptions.IgnoreCase)) {
contentDisposition = string.Join("; ", System.Text.RegularExpressions.Regex.Split(contentDisposition, @"; *"));
}
try {
ContentDisposition parser = new ContentDisposition(contentDisposition);
if (! string.IsNullOrEmpty(parser.FileName)) {
return parser.FileName;
}
} catch (FormatException) {
}
}
}
return NaGet.Utils.Url2filename(response.ResponseUri);
}
///
/// ダウンロード進捗メッセージを生成
///
/// 現在ダウンロード済みのサイズ
/// 全体のファイルサイズ。不明なときはゼロを指定。
/// ダウンロード開始からの時間をms単位で。
protected virtual void RaiseDownloadProgressTaskSetEvent(long downloadsize, long filesize, long elapsedms)
{
float percent = -1;
TimeSpan eta = new TimeSpan(0);
long byteps = 0;
// 進捗率の算出
if (filesize > 0) {
percent = 100 * ((float) downloadsize) / ((float) filesize);
}
// スループット・残り時間の算出
if (elapsedms > 0) {
byteps = 1000 * downloadsize / elapsedms;
if (filesize > 0 && byteps > 0) {
eta = TimeSpan.FromSeconds((filesize - downloadsize) / byteps);
}
}
System.Text.StringBuilder msgbuilder = new System.Text.StringBuilder();
msgbuilder.AppendFormat("{0} bytes", downloadsize);
if (percent > 0) {
msgbuilder.AppendFormat(" ({0:F2}%)", percent);
}
if (eta.Ticks > 0) {
msgbuilder.AppendFormat(" ETA {0}", eta);
}
if (byteps > 0) {
msgbuilder.AppendFormat(" ({0}/s)", NaGet.Utils.FormatSize(byteps));
}
RaiseTaskSetEvent(TaskEventType.PING, msgbuilder.ToString(), percent);
}
}
}