2 using System.Diagnostics;
6 using System.Threading;
9 namespace NaGet.SubCommands.SubTask
14 public class DownloadSubTask : NaGetSubTask
24 protected IWebProxy proxy;
29 protected string filepath;
34 protected WebRequest request;
37 /// レスポンスオブジェクト。応答がくるまではnullである。
39 protected WebResponse response;
42 /// ダウンロード時にHTTPヘッダなどから取得した本来のファイル名
44 private string downloadedFileName = null;
47 /// ダウンロード要求時のキャッシュレベル。デフォルトではキャッシュ無視
49 public System.Net.Cache.RequestCacheLevel CacheLevel = System.Net.Cache.RequestCacheLevel.NoCacheNoStore;
52 /// ダウンロード時に downloadedFileName に改名するか否か。
54 protected bool enableChangeFileName = false;
59 private bool cancelCalled = false;
62 /// ダウンロード中のファイル名につける接尾辞
64 private static readonly string PartialFilePostfix = ".part";
69 /// <param name="url">ダウンロード先URL</param>
70 /// <param name="filepath">保存ファイルパス</param>
71 /// <param name="proxy">プロキシ</param>
72 public DownloadSubTask(Uri url, string filepath, IWebProxy proxy)
75 this.filepath = filepath;
80 this.downloadedFileName = null;
83 public DownloadSubTask(string url, string filepath, IWebProxy proxy)
84 : this(new Uri(url), filepath, proxy)
91 /// <param name="url">ダウンロード先URL</param>
92 /// <param name="filepath">保存ファイルパス</param>
93 public DownloadSubTask(Uri url, string filepath)
94 : this(url, filepath, NaGet.Env.WebProxy)
98 public DownloadSubTask(string url, string filepath)
99 : this(new Uri(url), filepath, NaGet.Env.WebProxy)
104 /// ダウンロード時にHTTPヘッダなどから取得した本来のファイル名
106 public string DownloadedFileName {
107 get { return downloadedFileName; }
111 /// 保存先ファイル名を本来のファイル名に変えるか。
113 public bool EnableChangeFileName {
114 get { return enableChangeFileName; }
115 set { enableChangeFileName = value; }
121 public string Filepath {
122 get { return filepath; }
128 public override bool Cancelable {
129 get { return !this.cancelCalled; }
135 /// <returns>キャンセルに成功したときtrue</returns>
136 public override bool Cancel()
138 if (! this.cancelCalled && ! this.Done) {
139 this.cancelCalled = true;
140 if (request != null) {
143 } catch (WebException) {
152 public override void Run()
155 RaiseTaskSetEvent(TaskEventType.STARTED, string.Format("ダウンロード:{0}", this.url), 0);
159 runHandleCancelTrigger();
161 RaiseTaskSetEvent(TaskEventType.PING, string.Format("接続中...{0}", this.url.Host), -1);
163 runAcquireResponse();
164 runHandleCancelTrigger();
168 runHandleCancelTrigger();
172 RaiseTaskSetEvent(TaskEventType.COMPLETED, "ダウンロード終了", 100);
173 } catch (System.Net.WebException e) {
174 if (System.Net.NetworkInformation.NetworkInterface.GetIsNetworkAvailable()) {
175 RaiseTaskSetEvent(TaskEventType.WARNING, "ネットワークに接続されていません。", -1);
177 RaiseTaskSetEvent(TaskEventType.WARNING, "ネットワークに接続できませんでした。ネットワークが切断されているか、ファイアウォールによって遮断された可能性があります。", -1);
179 throw new System.Net.WebException(e.Message, e);
181 runReleaseResponse();
192 /// キャンセルされたかを確認して、キャンセル要求があるのならば TaskCanceledException を投げる
194 private void runHandleCancelTrigger()
196 if (this.cancelCalled) {
197 throw new TaskCanceledException(string.Empty);
204 private void runBuildRequest()
206 request = WebRequest.Create(url);
207 request.Proxy = this.proxy;
208 request.CachePolicy = new System.Net.Cache.RequestCachePolicy(CacheLevel);
210 HttpWebRequest httpRequest = request as HttpWebRequest;
211 if (httpRequest != null) {
212 httpRequest.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip;
213 httpRequest.UserAgent = NaGet.Env.UserAgentString;
220 private void runAcquireResponse()
223 response = request.GetResponse();
224 } catch (WebException e) {
225 if (cancelCalled) { // キャンセル時
226 throw new TaskCanceledException(string.Empty, e);
228 throw new WebException(e.Message, e);
236 private void runPrepareFile()
239 downloadedFileName = getFileNameFromWebResponse(response);
240 } catch (Exception) {
243 // パス名を変えるときは、HTTPヘッダから取得したファイル名に変更する。
244 if (enableChangeFileName && (!string.IsNullOrEmpty(downloadedFileName))) {
245 filepath = Path.Combine(Path.GetDirectoryName(filepath), downloadedFileName);
249 if (File.Exists(filepath)) {
250 File.Delete(filepath);
252 // 部分ファイルが存在するときも削除 TODO レジューム処理
253 if (File.Exists(filepath + PartialFilePostfix)) {
254 File.Delete(filepath + PartialFilePostfix);
258 private void runDownloadToFile()
260 Stopwatch stopwatch = new Stopwatch();
261 string partialFilepath = filepath + PartialFilePostfix;
263 using (Stream stream = response.GetResponseStream() )
264 using (FileStream fs = new FileStream(partialFilepath,
266 FileAccess.Write) ) {
269 File.SetAttributes(partialFilepath, FileAttributes.Hidden);
270 long contentLength = response.ContentLength;
273 RaiseDownloadProgressTaskSetEvent(0, contentLength, 0);
275 Timer timer = new Timer(new TimerCallback(
276 delegate(object obj) {
278 RaiseDownloadProgressTaskSetEvent(fs.Position, contentLength, stopwatch.ElapsedMilliseconds);
279 } catch (ObjectDisposedException) {
284 byte[] data = new byte[4096];
286 while ((size = stream.Read(data,0,data.Length)) > 0) {
287 fs.Write(data, 0, size);
290 throw new TaskCanceledException(string.Empty);
296 } catch (IOException ex) {
298 throw new TaskCanceledException(string.Empty);
300 throw new IOException(ex.Message, ex);
303 if (stopwatch != null) {
310 if (File.Exists(partialFilepath)) {
311 File.Move(partialFilepath, filepath);
312 File.SetAttributes(filepath, FileAttributes.Normal);
316 private void runPostprocess()
319 if (File.Exists(filepath)) {
320 HttpWebResponse httpResponse = response as HttpWebResponse;
321 FtpWebResponse ftpResponse = response as FtpWebResponse;
323 if (httpResponse != null) {
324 File.SetLastWriteTime(filepath, httpResponse.LastModified);
325 } else if (ftpResponse != null) {
326 File.SetLastWriteTime(filepath, ftpResponse.LastModified);
334 private void runReleaseResponse()
336 if (response != null) {
342 /// Webレスポンスからダウンロードしたファイルの名前を取得
344 /// <remarks>Content-Dispositionヘッダから取得あるいはURLの末尾から推定します</remarks>
345 /// <param name="response">レスポンスオブジェクト</param>
346 /// <returns>取得したファイル名</returns>
347 private static string getFileNameFromWebResponse(WebResponse response)
349 HttpWebResponse httpresp = response as HttpWebResponse;
350 if (httpresp != null) {
351 string contentDisposition = httpresp.Headers["Content-Disposition"];
353 if (! string.IsNullOrEmpty(contentDisposition)) {
354 // おかしな Content-Disposition ヘッダ向け
355 //(Content-Disposition: filename=\"...\" に attachment; を補って RFC1806 に準拠させる)
356 if (System.Text.RegularExpressions.Regex.IsMatch(contentDisposition, @" *filename=", System.Text.RegularExpressions.RegexOptions.IgnoreCase)) {
357 contentDisposition = "attachment; " + contentDisposition;
359 // "atachment;filename=\""のようにセミコロンの後ろにスペースがない場合、それを補充する
360 if (System.Text.RegularExpressions.Regex.IsMatch(contentDisposition, @";[^ ]", System.Text.RegularExpressions.RegexOptions.IgnoreCase)) {
361 contentDisposition = string.Join("; ", System.Text.RegularExpressions.Regex.Split(contentDisposition, @"; *"));
365 ContentDisposition parser = new ContentDisposition(contentDisposition);
366 if (! string.IsNullOrEmpty(parser.FileName)) {
367 return parser.FileName;
369 } catch (FormatException) {
374 return NaGet.Utils.Url2filename(response.ResponseUri);
380 /// <param name="downloadsize">現在ダウンロード済みのサイズ</param>
381 /// <param name="filesize">全体のファイルサイズ。不明なときはゼロを指定。</param>
382 /// <param name="elapsedms">ダウンロード開始からの時間をms単位で。</param>
383 protected virtual void RaiseDownloadProgressTaskSetEvent(long downloadsize, long filesize, long elapsedms)
386 TimeSpan eta = new TimeSpan(0);
391 percent = 100 * ((float) downloadsize) / ((float) filesize);
396 byteps = 1000 * downloadsize / elapsedms;
397 if (filesize > 0 && byteps > 0) {
398 eta = TimeSpan.FromSeconds((filesize - downloadsize) / byteps);
402 System.Text.StringBuilder msgbuilder = new System.Text.StringBuilder();
403 msgbuilder.AppendFormat("{0} bytes", downloadsize);
405 msgbuilder.AppendFormat(" ({0:F2}%)", percent);
408 msgbuilder.AppendFormat(" ETA {0}", eta);
411 msgbuilder.AppendFormat(" ({0}/s)", NaGet.Utils.FormatSize(byteps));
414 RaiseTaskSetEvent(TaskEventType.PING, msgbuilder.ToString(), percent);