namespace VideoBrowser.Core { using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.IO; using System.Net; using System.Threading; using VideoBrowser.Common; /// /// Defines the /// public class FileDownloader : IDisposable { #region Fields private readonly bool _disposed = false; private BackgroundWorker _downloader; #endregion Fields #region Constructors /// /// Initializes a new instance of the class. /// public FileDownloader() { _downloader = new BackgroundWorker() { WorkerReportsProgress = true, WorkerSupportsCancellation = true }; _downloader.DoWork += downloader_DoWork; _downloader.ProgressChanged += downloader_ProgressChanged; _downloader.RunWorkerCompleted += downloader_RunWorkerCompleted; this.DeleteUnfinishedFilesOnCancel = true; this.Files = new List(); } #endregion Constructors #region Delegates /// /// The FileDownloadEventHandler /// /// The sender /// The e public delegate void FileDownloadEventHandler(object sender, FileDownloadEventArgs e); /// /// The FileDownloadFailedEventHandler /// /// The sender /// The e public delegate void FileDownloadFailedEventHandler(object sender, FileDownloadFailedEventArgs e); #endregion Delegates #region Events /// /// Defines the CalculatedTotalFileSize /// public event EventHandler CalculatedTotalFileSize; /// /// Defines the Canceled /// public event EventHandler Canceled; /// /// Defines the Completed /// public event EventHandler Completed; /// /// Defines the FileDownloadComplete /// public event FileDownloadEventHandler FileDownloadComplete; /// /// Defines the FileDownloadFailed /// public event FileDownloadFailedEventHandler FileDownloadFailed; /// /// Defines the FileDownloadSucceeded /// public event FileDownloadEventHandler FileDownloadSucceeded; /// /// Defines the Paused /// public event EventHandler Paused; /// /// Defines the ProgressChanged /// public event EventHandler ProgressChanged; /// /// Defines the Resumed /// public event EventHandler Resumed; /// /// Defines the Started /// public event EventHandler Started; /// /// Defines the Stopped /// public event EventHandler Stopped; #endregion Events #region Enums /// /// Defines the BackgroundEvents /// private enum BackgroundEvents { /// /// Defines the CalculatedTotalFileSize /// CalculatedTotalFileSize, /// /// Defines the FileDownloadComplete /// FileDownloadComplete, /// /// Defines the FileDownloadSucceeded /// FileDownloadSucceeded, /// /// Defines the ProgressChanged /// ProgressChanged } #endregion Enums #region Properties /// /// Gets a value indicating whether CanPause /// public bool CanPause => this.IsBusy && !this.IsPaused && !_downloader.CancellationPending; /// /// Gets a value indicating whether CanResume /// public bool CanResume => this.IsBusy && this.IsPaused && !_downloader.CancellationPending; /// /// Gets a value indicating whether CanStart /// public bool CanStart => !this.IsBusy; /// /// Gets a value indicating whether CanStop /// public bool CanStop => this.IsBusy && !_downloader.CancellationPending; /// /// Gets the CurrentFile /// public FileDownload CurrentFile { get; private set; } /// /// Gets or sets a value indicating whether DeleteUnfinishedFilesOnCancel /// public bool DeleteUnfinishedFilesOnCancel { get; set; } /// /// Gets or sets the Files /// public List Files { get; set; } /// /// Gets a value indicating whether IsBusy /// public bool IsBusy { get; private set; } /// /// Gets a value indicating whether IsPaused /// public bool IsPaused { get; private set; } /// /// Gets or sets the PackageSize /// public int PackageSize { get; set; } = 4096; /// /// Gets or sets the Speed /// public int Speed { get; set; } /// /// Gets or sets the TotalProgress /// public long TotalProgress { get; set; } /// /// Gets the TotalSize /// public long TotalSize { get; private set; } /// /// Gets or sets a value indicating whether WasCanceled /// public bool WasCanceled { get; set; } #endregion Properties #region Methods /// /// The Dispose /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// The Pause /// public void Pause() { if (!this.IsBusy || this.IsPaused) { return; } this.IsPaused = true; this.OnPaused(); } /// /// The Resume /// public void Resume() { if (!this.IsBusy || !this.IsPaused) { return; } this.IsPaused = false; this.OnResumed(); } /// /// The Start /// public void Start() { this.IsBusy = true; this.WasCanceled = false; this.TotalProgress = 0; _downloader.RunWorkerAsync(); this.OnStarted(); } /// /// The Stop /// public void Stop() { this.IsBusy = false; this.IsPaused = false; this.WasCanceled = true; _downloader.CancelAsync(); this.OnCanceled(); } /// /// The TotalPercentage /// /// The public double TotalPercentage() { return Math.Round((double)this.TotalProgress / this.TotalSize * 100, 2); } /// /// The Dispose /// /// The disposing protected virtual void Dispose(bool disposing) { if (!_disposed) { if (disposing) { // Free other state (managed objects) _downloader.Dispose(); _downloader.DoWork -= downloader_DoWork; _downloader.ProgressChanged -= downloader_ProgressChanged; _downloader.RunWorkerCompleted -= downloader_RunWorkerCompleted; } // Free your own state (unmanaged objects) // Set large fields to null this.Files = null; } } /// /// The OnCalculatedTotalFileSize /// protected void OnCalculatedTotalFileSize() => this.CalculatedTotalFileSize?.Invoke(this, EventArgs.Empty); /// /// The OnCanceled /// protected void OnCanceled() => this.Canceled?.Invoke(this, EventArgs.Empty); /// /// The OnCompleted /// protected void OnCompleted() => this.Completed?.Invoke(this, EventArgs.Empty); /// /// The OnFileDownloadComplete /// /// The e protected void OnFileDownloadComplete(FileDownloadEventArgs e) => this.FileDownloadComplete?.Invoke(this, e); /// /// The OnFileDownloadFailed /// /// The exception /// The fileDownload protected void OnFileDownloadFailed(Exception exception, FileDownload fileDownload) => this.FileDownloadFailed?.Invoke(this, new FileDownloadFailedEventArgs(exception, fileDownload)); /// /// The OnFileDownloadSucceeded /// /// The e protected void OnFileDownloadSucceeded(FileDownloadEventArgs e) => this.FileDownloadSucceeded?.Invoke(this, e); /// /// The OnPaused /// protected void OnPaused() => this.Paused?.Invoke(this, EventArgs.Empty); /// /// The OnProgressChanged /// protected void OnProgressChanged() => this.ProgressChanged?.Invoke(this, EventArgs.Empty); /// /// The OnResumed /// protected void OnResumed() => this.Resumed?.Invoke(this, EventArgs.Empty); /// /// The OnStarted /// protected void OnStarted() => this.Started?.Invoke(this, EventArgs.Empty); /// /// The OnStopped /// protected void OnStopped() => this.Stopped?.Invoke(this, EventArgs.Empty); /// /// The CalculateTotalFileSize /// private void CalculateTotalFileSize() { this.TotalSize = 0; foreach (var file in this.Files) { try { var webReq = WebRequest.Create(file.Url); var webResp = webReq.GetResponse(); this.TotalSize += webResp.ContentLength; webResp.Close(); } catch (Exception) { } } _downloader.ReportProgress(-1, BackgroundEvents.CalculatedTotalFileSize); } /// /// The CleanupFiles /// private void CleanupFiles() { if (this.Files == null) { Logger.Info("Clean up files is canceled, Files property is null."); return; } var files = this.Files; new Thread(delegate () { var dict = new Dictionary(); var keys = new List(); foreach (var file in files) { if (file.AlwaysCleanupOnCancel || !file.IsFinished) { dict.Add(file.Path, 0); keys.Add(file.Path); } } while (dict.Count > 0) { foreach (string key in keys) { try { if (File.Exists(key)) { File.Delete(key); } // Remove file from dictionary since it either got deleted // or it doesn't exist anymore. dict.Remove(key); } catch { if (dict[key] == 10) { dict.Remove(key); } else { dict[key]++; } } } Thread.Sleep(2000); } }).Start(); } /// /// The downloader_DoWork /// /// The sender /// The e private void downloader_DoWork(object sender, DoWorkEventArgs e) { this.CalculateTotalFileSize(); foreach (var file in this.Files) { this.CurrentFile = file; if (!Directory.Exists(file.Directory)) { Directory.CreateDirectory(file.Directory); } this.DownloadFile(); this.RaiseEventFromBackground(BackgroundEvents.FileDownloadComplete, new FileDownloadEventArgs(file)); if (_downloader.CancellationPending) { this.CleanupFiles(); } } } /// /// The downloader_ProgressChanged /// /// The sender /// The e private void downloader_ProgressChanged(object sender, ProgressChangedEventArgs e) { if (e.UserState is Exception) { this.CurrentFile.Exception = e.UserState as Exception; this.OnFileDownloadFailed(e.UserState as Exception, this.CurrentFile); } else if (e.UserState is object[]) { var obj = (object[])e.UserState; if (obj[0] is BackgroundEvents) { this.RaiseEvent((BackgroundEvents)obj[0], (EventArgs)obj[1]); } } } /// /// The downloader_RunWorkerCompleted /// /// The sender /// The e private void downloader_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { this.IsBusy = this.IsPaused = false; if (!this.WasCanceled) { this.OnCompleted(); } else { this.OnCanceled(); } this.OnStopped(); } /// /// The DownloadFile /// private void DownloadFile() { long totalSize = 0; var readBytes = new byte[this.PackageSize]; int currentPackageSize; var speedTimer = new Stopwatch(); Exception exception = null; long existLen = 0; if (File.Exists(this.CurrentFile.Path)) { existLen = new FileInfo(this.CurrentFile.Path).Length; } FileStream writer = existLen > 0 ? new FileStream(this.CurrentFile.Path, FileMode.Append, FileAccess.Write) : new FileStream(this.CurrentFile.Path, FileMode.Create, FileAccess.Write); HttpWebRequest webReq; HttpWebResponse webResp = null; try { webReq = (HttpWebRequest)WebRequest.Create(this.CurrentFile.Url); webReq.Method = "HEAD"; webResp = (HttpWebResponse)webReq.GetResponse(); if (webResp.ContentLength == existLen) { totalSize = existLen; } else { webResp.Close(); webReq = (HttpWebRequest)WebRequest.Create(this.CurrentFile.Url); webReq.AddRange(existLen); webResp = (HttpWebResponse)webReq.GetResponse(); totalSize = existLen + webResp.ContentLength; } } catch (Exception ex) { webResp?.Close(); webResp?.Dispose(); exception = ex; } this.CurrentFile.TotalFileSize = totalSize; if (exception != null) { _downloader.ReportProgress(0, exception); } else { this.CurrentFile.Progress = existLen; this.TotalProgress += existLen; long prevSize = 0; var speedInterval = 100; var stream = webResp.GetResponseStream(); while (this.CurrentFile.Progress < totalSize && !_downloader.CancellationPending) { while (this.IsPaused) { Thread.Sleep(100); } speedTimer.Start(); currentPackageSize = stream.Read(readBytes, 0, this.PackageSize); this.CurrentFile.Progress += currentPackageSize; this.TotalProgress += currentPackageSize; // Raise ProgressChanged event this.RaiseEventFromBackground(BackgroundEvents.ProgressChanged, EventArgs.Empty); writer.Write(readBytes, 0, currentPackageSize); if (speedTimer.Elapsed.TotalMilliseconds >= speedInterval) { var downloadedBytes = writer.Length - prevSize; prevSize = writer.Length; this.Speed = (int)downloadedBytes * (speedInterval == 100 ? 10 : 1); // Only update speed once a second after initial update speedInterval = 1000; speedTimer.Reset(); } } speedTimer.Stop(); stream.Close(); writer.Close(); webResp.Close(); webResp.Dispose(); if (!_downloader.CancellationPending) { this.CurrentFile.IsFinished = true; this.RaiseEventFromBackground(BackgroundEvents.FileDownloadSucceeded, new FileDownloadEventArgs(this.CurrentFile)); } } } /// /// The RaiseEvent /// /// The evt /// The e private void RaiseEvent(BackgroundEvents evt, EventArgs e) { switch (evt) { case BackgroundEvents.CalculatedTotalFileSize: this.OnCalculatedTotalFileSize(); break; case BackgroundEvents.FileDownloadComplete: this.OnFileDownloadComplete((FileDownloadEventArgs)e); break; case BackgroundEvents.FileDownloadSucceeded: this.OnFileDownloadSucceeded((FileDownloadEventArgs)e); break; case BackgroundEvents.ProgressChanged: this.OnProgressChanged(); break; } } /// /// The RaiseEventFromBackground /// /// The evt /// The e private void RaiseEventFromBackground(BackgroundEvents evt, EventArgs e) => _downloader.ReportProgress(-1, new object[] { evt, e }); #endregion Methods } }