OSDN Git Service

AppliStation-All,AssemblyVersionを1.4.0に
[applistation/AppliStation.git] / na-get-lib / NaGet.SubCommands.SubTask / DownloadSubTask.cs
1 using System;
2 using System.Diagnostics;
3 using System.Net;
4 using System.Net.Mime;
5 using System.IO;
6 using System.Threading;
7 using NaGet.Tasks;
8
9 namespace NaGet.SubCommands.SubTask
10 {
11         /// <summary>
12         /// ダウンロードタスク
13         /// </summary>
14         public class DownloadSubTask : NaGetSubTask
15         {
16                 /// <summary>
17                 /// アクセスURL
18                 /// </summary>
19                 protected Uri url;
20                 
21                 /// <summary>
22                 /// プロキシ
23                 /// </summary>
24                 protected IWebProxy proxy;
25                 
26                 /// <summary>
27                 /// 保存先
28                 /// </summary>
29                 protected string filepath;
30                 
31                 /// <summary>
32                 /// リクエストオブジェクト
33                 /// </summary>
34                 protected WebRequest request;
35                 
36                 /// <summary>
37                 /// レスポンスオブジェクト。応答がくるまではnullである。
38                 /// </summary>
39                 protected WebResponse response;
40                 
41                 /// <summary>
42                 /// ダウンロード時にHTTPヘッダなどから取得した本来のファイル名
43                 /// </summary>
44                 private string downloadedFileName = null;
45                 
46                 /// <summary>
47                 /// ダウンロード要求時のキャッシュレベル。デフォルトではキャッシュ無視
48                 /// </summary>
49                 public System.Net.Cache.RequestCacheLevel CacheLevel = System.Net.Cache.RequestCacheLevel.NoCacheNoStore;
50                 
51                 /// <summary>
52                 /// ダウンロード時に downloadedFileName に改名するか否か。
53                 /// </summary>
54                 protected bool enableChangeFileName = false;
55                 
56                 /// <summary>
57                 /// キャンセルが呼ばれたか否か。
58                 /// </summary>
59                 private bool cancelCalled = false;
60                 
61                 /// <summary>
62                 /// ダウンロード中のファイル名につける接尾辞
63                 /// </summary>
64                 private static readonly string PartialFilePostfix = ".part";
65                 
66                 /// <summary>
67                 /// コンストラクタ
68                 /// </summary>
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)
73                 {
74                         this.url = url;
75                         this.filepath = filepath;
76                         this.proxy = proxy;
77                         
78                         this.request = null;
79                         this.response = null;
80                         this.downloadedFileName = null;
81                 }
82                 
83                 public DownloadSubTask(string url, string filepath, IWebProxy proxy)
84                         : this(new Uri(url), filepath, proxy)
85                 {
86                 }
87                 
88                 /// <summary>
89                 /// コンストラクタ
90                 /// </summary>
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)
95                 {
96                 }
97                 
98                 public DownloadSubTask(string url, string filepath)
99                         : this(new Uri(url), filepath, NaGet.Env.WebProxy)
100                 {
101                 }
102         
103                 /// <summary>
104                 /// ダウンロード時にHTTPヘッダなどから取得した本来のファイル名
105                 /// </summary>
106                 public string DownloadedFileName {
107                         get { return downloadedFileName; }
108                 }
109                 
110                 /// <summary>
111                 /// 保存先ファイル名を本来のファイル名に変えるか。
112                 /// </summary>
113                 public bool EnableChangeFileName {
114                         get { return enableChangeFileName; }
115                         set { enableChangeFileName = value; }
116                 }
117                 
118                 /// <summary>
119                 /// 保存ファイル。
120                 /// </summary>
121                 public string Filepath {
122                         get { return filepath; }
123                 }
124                 
125                 /// <summary>
126                 /// キャンセル可能
127                 /// </summary>
128                 public override bool Cancelable {
129                         get { return !this.cancelCalled; }
130                 }
131                 
132                 /// <summary>
133                 /// ダウンロード処理をキャンセルする
134                 /// </summary>
135                 /// <returns>キャンセルに成功したときtrue</returns>
136                 public override bool Cancel()
137                 {
138                         if (! this.cancelCalled && ! this.Done) {
139                                 this.cancelCalled = true;
140                                 if (request != null) {
141                                         try {
142                                                 request.Abort();
143                                         } catch (WebException) {
144                                         }
145                                 }
146                                 return true;
147                         } else {
148                                 return false;
149                         }
150                 }
151                 
152                 public override void Run()
153                 {
154                         NotifyStarted();
155                         RaiseTaskSetEvent(TaskEventType.STARTED, string.Format("ダウンロード:{0}", this.url), 0);
156                         
157                         try {
158                                 runBuildRequest();
159                                 runHandleCancelTrigger();
160                                 
161                                 RaiseTaskSetEvent(TaskEventType.PING, string.Format("接続中...{0}", this.url.Host), -1);
162                                 
163                                 runAcquireResponse();
164                                 runHandleCancelTrigger();
165                                 
166                                 runPrepareFile();
167                                 runDownloadToFile();
168                                 runHandleCancelTrigger();
169                                 
170                                 runPostprocess();
171                                 
172                                 RaiseTaskSetEvent(TaskEventType.COMPLETED, "ダウンロード終了", 100);
173                         } catch (System.Net.WebException e) {
174                                 if (System.Net.NetworkInformation.NetworkInterface.GetIsNetworkAvailable()) {
175                                         RaiseTaskSetEvent(TaskEventType.WARNING, "ネットワークに接続されていません。", -1);
176                                 } else {
177                                         RaiseTaskSetEvent(TaskEventType.WARNING, "ネットワークに接続できませんでした。ネットワークが切断されているか、ファイアウォールによって遮断された可能性があります。", -1);
178                                 }
179                                 throw new System.Net.WebException(e.Message, e);
180                         } finally {
181                                 runReleaseResponse();
182                                 
183                                 if (cancelCalled) {
184                                         NotifyCancelled();
185                                 } else {
186                                         NotifyCompleted();
187                                 }
188                         }
189                 }
190                 
191                 /// <summary>
192                 /// キャンセルされたかを確認して、キャンセル要求があるのならば TaskCanceledException を投げる
193                 /// </summary>
194                 private void runHandleCancelTrigger()
195                 {
196                         if (this.cancelCalled) {
197                                 throw new TaskCanceledException(string.Empty);
198                         }
199                 }
200                 
201                 /// <summary>
202                 /// requestの構築。
203                 /// </summary>
204                 private void runBuildRequest()
205                 {
206                         request = WebRequest.Create(url);
207                         request.Proxy = this.proxy;
208                         request.CachePolicy = new System.Net.Cache.RequestCachePolicy(CacheLevel);
209                         
210                         HttpWebRequest httpRequest = request as HttpWebRequest;
211                         if (httpRequest != null) {
212                                 httpRequest.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip;
213                                 httpRequest.UserAgent = NaGet.Env.UserAgentString;
214                         }
215                 }
216                 
217                 /// <summary>
218                 /// Responseを得る
219                 /// </summary>
220                 private void runAcquireResponse()
221                 {
222                         try {
223                                 response = request.GetResponse();
224                         } catch (WebException e) {
225                                 if (cancelCalled) { // キャンセル時
226                                         throw new TaskCanceledException(string.Empty, e);
227                                 } else {
228                                         throw new WebException(e.Message, e);
229                                 }
230                         }
231                 }
232                 
233                 /// <summary>
234                 /// ダウンロード先ファイル名の決定
235                 /// </summary>
236                 private void runPrepareFile()
237                 {
238                         try {
239                                 downloadedFileName = getFileNameFromWebResponse(response);
240                         } catch (Exception) {
241                         }
242                         
243                         // パス名を変えるときは、HTTPヘッダから取得したファイル名に変更する。
244                         if (enableChangeFileName && (!string.IsNullOrEmpty(downloadedFileName))) {
245                                 filepath = Path.Combine(Path.GetDirectoryName(filepath), downloadedFileName);
246                         }
247                         
248                         // ファイルが存在するとき削除
249                         if (File.Exists(filepath)) {
250                                 File.Delete(filepath);
251                         }
252                         // 部分ファイルが存在するときも削除 TODO レジューム処理
253                         if (File.Exists(filepath + PartialFilePostfix)) {
254                                 File.Delete(filepath + PartialFilePostfix);
255                         }
256                 }
257                 
258                 private void runDownloadToFile()
259                 {
260                         Stopwatch stopwatch = new Stopwatch();
261                         string partialFilepath = filepath + PartialFilePostfix;
262                         
263                         using (Stream stream = response.GetResponseStream() )
264                         using (FileStream fs = new FileStream(partialFilepath,
265                                                     FileMode.Create,
266                                                     FileAccess.Write) ) {
267                                 
268                                 try {
269                                         File.SetAttributes(partialFilepath, FileAttributes.Hidden);
270                                         long contentLength = response.ContentLength;
271         
272                                         stopwatch.Start();
273                                         RaiseDownloadProgressTaskSetEvent(0, contentLength, 0);
274         
275                                         Timer timer = new Timer(new TimerCallback(
276                                                 delegate(object obj) {
277                                                         try {
278                                                                 RaiseDownloadProgressTaskSetEvent(fs.Position, contentLength, stopwatch.ElapsedMilliseconds);
279                                                         } catch (ObjectDisposedException) {
280                                                         }
281                                                 }), null, 0, 1000);
282                                         
283                                         try {
284                                                 byte[] data = new byte[4096];
285                                                 int size = 0;
286                                                 while ((size = stream.Read(data,0,data.Length)) > 0) {
287                                                         fs.Write(data, 0, size);
288                                                         
289                                                         if (cancelCalled) {
290                                                                 throw new TaskCanceledException(string.Empty);
291                                                         }
292                                                 }
293                                         } finally {
294                                                 timer.Dispose();
295                                         }
296                                 } catch (IOException ex) {
297                                         if (cancelCalled) {
298                                                 throw new TaskCanceledException(string.Empty);
299                                         } else {
300                                                 throw new IOException(ex.Message, ex);
301                                         }
302                                 } finally {
303                                         if (stopwatch != null) {
304                                                 stopwatch.Stop();
305                                                 stopwatch = null;
306                                         }
307                                 }
308                         }
309                         
310                         if (File.Exists(partialFilepath)) {
311                                 File.Move(partialFilepath, filepath);
312                                 File.SetAttributes(filepath, FileAttributes.Normal);
313                         }
314                 }
315                 
316                 private void runPostprocess()
317                 {
318                         // 更新日を補完
319                         if (File.Exists(filepath)) {
320                                 HttpWebResponse httpResponse = response as HttpWebResponse;
321                                 FtpWebResponse  ftpResponse      = response as FtpWebResponse;
322                                 
323                                 if (httpResponse != null) {
324                                         File.SetLastWriteTime(filepath, httpResponse.LastModified);
325                                 } else if (ftpResponse != null) {
326                                         File.SetLastWriteTime(filepath, ftpResponse.LastModified);
327                                 }
328                         }
329                 }
330                 
331                 /// <summary>
332                 /// responseの開放
333                 /// </summary>
334                 private void runReleaseResponse()
335                 {
336                         if (response != null) {
337                                 response.Close();
338                         }
339                 }
340                 
341                 /// <summary>
342                 /// Webレスポンスからダウンロードしたファイルの名前を取得
343                 /// </summary>
344                 /// <remarks>Content-Dispositionヘッダから取得あるいはURLの末尾から推定します</remarks>
345                 /// <param name="response">レスポンスオブジェクト</param>
346                 /// <returns>取得したファイル名</returns>
347                 private static string getFileNameFromWebResponse(WebResponse response)
348                 {
349                         HttpWebResponse httpresp = response as HttpWebResponse;
350                         if (httpresp != null) {
351                                 string contentDisposition = httpresp.Headers["Content-Disposition"];
352                                 
353                                 if (! string.IsNullOrEmpty(contentDisposition)) {
354                                         try {
355                                                 ContentDisposition parser = new ContentDisposition(contentDisposition);
356                                                 if (! string.IsNullOrEmpty(parser.FileName)) {
357                                                         return parser.FileName;
358                                                 }
359                                         } catch (FormatException) {
360                                         }
361                                 }
362                         }
363                         
364                         return NaGet.Utils.Url2filename(response.ResponseUri);
365                 }
366                 
367                 /// <summary>
368                 /// ダウンロード進捗メッセージを生成
369                 /// </summary>
370                 /// <param name="downloadsize">現在ダウンロード済みのサイズ</param>
371                 /// <param name="filesize">全体のファイルサイズ。不明なときはゼロを指定。</param>
372                 /// <param name="elapsedms">ダウンロード開始からの時間をms単位で。</param>
373                 protected virtual void RaiseDownloadProgressTaskSetEvent(long downloadsize, long filesize, long elapsedms)
374                 {
375                         float percent = -1;
376                         TimeSpan eta = new TimeSpan(0);
377                         long byteps = 0;
378                         
379                         // 進捗率の算出
380                         if (filesize > 0) {
381                                 percent = 100 * ((float) downloadsize) / ((float) filesize);
382                         }
383                         
384                         // スループット・残り時間の算出
385                         if (elapsedms > 0) {
386                                 byteps = 1000 * downloadsize / elapsedms;
387                                 if (filesize > 0 && byteps > 0) {
388                                         eta = TimeSpan.FromSeconds((filesize - downloadsize) / byteps);
389                                 }
390                         }
391                         
392                         System.Text.StringBuilder msgbuilder = new System.Text.StringBuilder();
393                         msgbuilder.AppendFormat("{0} bytes", downloadsize);
394                         if (percent > 0) {
395                                 msgbuilder.AppendFormat(" ({0:F2}%)", percent);
396                         }
397                         if (eta.Ticks > 0) {
398                                 msgbuilder.AppendFormat(" ETA {0}", eta);
399                         }
400                         if (byteps > 0) {
401                                 msgbuilder.AppendFormat(" ({0}/s)", NaGet.Utils.FormatSize(byteps));
402                         }
403                         
404                         RaiseTaskSetEvent(TaskEventType.PING, msgbuilder.ToString(), percent);
405                 }
406         }
407 }