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 ヘッダ向け // attachment と書いていないでいきなり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); } } }