/// <summary> /// Instantiates a new instance of the DownloadTask.DownloadProgress class. /// </summary> /// <param name="percent">Percent completed.</param> /// <param name="bytes">Number of bytes downloaded so far.</param> /// <param name="totalBytes">Total number of bytes to be downloaded, i.e. the file size.</param> /// <param name="speed">Download speed.</param> /// <param name="eta">Estimated time of arrival (completion).</param> public DownloadProgress(double percent, FileSize bytes, FileSize totalBytes, DownloadSpeed speed, TimeSpan eta, TimeSpan elapsed) { _percentComplete = percent; _bytesDownloaded = bytes; _totalBytes = totalBytes; _speed = speed; _eta = eta; _elapsed = elapsed; }
/// <summary> /// Starts downloading the file. /// </summary> /// <param name="saveLocation">Path to where the file will be saved.</param> /// <param name="onProgressChanged">Interface to track download progress.</param> /// <returns>A System.Threading.Task that represents the download operation.</returns> /// <exception cref="System.NotSupportedException"> /// Invalid URI scheme. /// </exception> /// <exception cref="System.Security.SecurityException"> /// Permission is not granted to connect to remote server. /// </exception> /// <exception cref="System.UriFormatException">The download URL is invalid.</exception> /// <exception cref="System.Net.WebException">An error occurred while processing the request.</exception> /// <exception cref="System.ArgumentException"> /// path is an empty string (""), contains only white space, or contains one /// or more invalid characters. /// </exception> /// <exception cref="System.IO.IOException"> /// An I/O error, such as specifying FileMode.CreateNew when the file specified /// by path already exists, occurred. -or-The stream has been closed. /// </exception> /// <exception cref="System.Security.SecurityException"> /// The caller does not have the required permission. /// </exception> /// <exception cref="System.IO.DirectoryNotFoundException"> /// The specified path is invalid, such as being on an unmapped drive. /// </exception> /// <exception cref="System.UnauthorizedAccessException"> /// The access requested is not permitted by the operating system for the specified /// path, such as when access is Write or ReadWrite and the file or directory /// is set for read-only access. /// </exception> /// <exception cref="System.IO.PathTooLongException"> /// The specified path, file name, or both exceed the system-defined maximum /// length. For example, on Windows-based platforms, paths must be less than /// 248 characters, and file names must be less than 260 characters. /// </exception> public Task DownloadAsync(string saveLocation, IProgress <DownloadProgress> onProgressChanged = null) { if (!IsInitialized) { throw new InvalidOperationException("The object has not been initialized. You must call DownloadTask.Initialize() first."); } if (isDisposed) { throw new ObjectDisposedException("DownloadTask", "Cannot start because this instance has been disposed."); } if (Status == DownloadTaskStatus.Failed || Status == DownloadTaskStatus.Canceled) { downloadWatch.Reset(); } Status = DownloadTaskStatus.Starting; downloadWatch.Start(); elapsedTimer.Start(); FullPath = saveLocation; // filename after download FileName = Path.GetFileName(FullPath); _saveToIncomlete = FullPath + IncompleteDlExt; // save here temporarily (until download completes) progressReporter = onProgressChanged; // save so that it can be used for resumption cancelSource = new CancellationTokenSource(); // disposed after paused, canceled, completed. a new one is use when resuming return(Task.Factory.StartNew(() => { HttpWebResponse response = null; FileStream fStream = null; try { HttpWebRequest request = (HttpWebRequest)WebRequest.Create(_url); request.UseDefaultCredentials = true; request.Proxy = WebRequest.GetSystemWebProxy(); byte[] buffer = new byte[BufferSize]; FileMode mode; long bytesDownloaded = 0; if (File.Exists(_saveToIncomlete) && ResumeSupported) // dl has been started before; try to resume { FileInfo fInfo = new FileInfo(_saveToIncomlete); if (fInfo.Length < this.Size.Value) // incomplete file must be smaller than total download size // add range header to resume where left off { request.AddRange(fInfo.Length); bytesDownloaded = fInfo.Length; mode = FileMode.Append; } else // otherwise, it's another file with the same name or it got currupted { File.Delete(_saveToIncomlete); mode = FileMode.Create; } } else // resume not supported; start from beginning { mode = FileMode.Create; } // begin download fStream = new FileStream(_saveToIncomlete, mode, FileAccess.Write); response = (HttpWebResponse)request.GetResponse(); using (Stream dlStream = response.GetResponseStream()) { // download stream // for measuring speed const int reportInterval = 200; // ms int tmpBytes = 0; Stopwatch watch = new Stopwatch(); Func <DownloadProgress> ProgressSnapshot = () => { double percent; TimeSpan eta; FileSize totalSize; FileSize curSize = new FileSize(bytesDownloaded); DownloadSpeed speed; if (Status == DownloadTaskStatus.Completed) { speed = new DownloadSpeed(Size.Value, Elapsed); } else { speed = new DownloadSpeed(tmpBytes, watch.Elapsed); // current speed } if (Size.Value > 0) // got file size from server { totalSize = new FileSize(this.Size.Value); percent = (double)bytesDownloaded / Size.Value * 100; eta = TimeSpan.FromSeconds((this.Size.Value - bytesDownloaded) / (bytesDownloaded / Elapsed.TotalSeconds)); // eta from average speed } else // size is not known (happens with webpages) { totalSize = new FileSize(bytesDownloaded); // use number of bytes downloaded so far as size percent = -1; eta = TimeSpan.FromSeconds(-1); } return new DownloadProgress(percent, curSize, totalSize, speed, eta, Elapsed); }; while (true) { Status = DownloadTaskStatus.Downloading; watch.Start(); int bytesRead = dlStream.Read(buffer, 0, buffer.Length); watch.Stop(); if (watch.ElapsedMilliseconds >= reportInterval) { lock (Progress) { Progress.Update(ProgressSnapshot()); } if (onProgressChanged != null) { onProgressChanged.Report(Progress); } tmpBytes = 0; watch.Reset(); } bytesDownloaded += bytesRead; // total downloaded so far tmpBytes += bytesRead; // total since last progress report if (bytesRead < 1) // dl completed or failed { if (fStream.Length < Size.Value) // downloaded less than required { Status = DownloadTaskStatus.Failed; } else { Status = DownloadTaskStatus.Completed; } lock (Progress) { Progress.Update(ProgressSnapshot()); } if (onProgressChanged != null) { onProgressChanged.Report(Progress); } break; } // write block to file fStream.Write(buffer, 0, bytesRead); fStream.Flush(); try { // check for cancellation (pause or cancel) cancelSource.Token.ThrowIfCancellationRequested(); } catch (OperationCanceledException) { if (Status == DownloadTaskStatus.Pausing) { Status = DownloadTaskStatus.Paused; } else { Status = DownloadTaskStatus.Canceled; } throw; } } } } catch (Exception ex) { if (!(ex is OperationCanceledException)) // something went wrong { Status = DownloadTaskStatus.Failed; throw; } } finally { downloadWatch.Stop(); elapsedTimer.Stop(); if (response != null) { response.Dispose(); } if (fStream != null) { fStream.Dispose(); } if (cancelSource != null) { cancelSource.Dispose(); cancelSource = null; } if (Status == DownloadTaskStatus.Completed) { File.Move(_saveToIncomlete, _saveTo); // rename temporary file } else if (Status == DownloadTaskStatus.Canceled) { if (DeleteWhenCanceled) { File.Delete(_saveToIncomlete); } } } }, TaskCreationOptions.LongRunning)); }