├── .gitattributes ├── .gitignore ├── .nuget └── NuGet.Config ├── Downloader.Business.Contract ├── DownloadCheckResult.cs ├── DownloadRange.cs ├── Downloader.Business.Contract.csproj ├── Enums │ ├── DownloadState.cs │ └── DownloadStopType.cs ├── Events │ ├── DownloadCancelledEventArgs.cs │ ├── DownloadDataReceivedArgs.cs │ ├── DownloadDelegates.cs │ ├── DownloadEventArgs.cs │ └── DownloadStartedEventArgs.cs ├── Exceptions │ ├── DownloadCheckNotSuccessfulException.cs │ ├── ResumingNotSupportedException.cs │ └── TooManyRetriesException.cs ├── IDownload.cs ├── IDownloadBuilder.cs ├── IDownloadChecker.cs ├── IDownloadObserver.cs ├── IWebRequestBuilder.cs └── Properties │ └── AssemblyInfo.cs ├── Downloader.Business ├── Download │ ├── AbstractDownload.cs │ ├── MultiPartDownload.cs │ ├── ResumingDownload.cs │ └── SimpleDownload.cs ├── DownloadBuilder │ ├── MultiPartDownloadBuilder.cs │ ├── ResumingDownloadBuilder.cs │ └── SimpleDownloadBuilder.cs ├── Downloader.Business.csproj ├── Observer │ ├── AbstractDownloadObserver.cs │ ├── DownloadDataSample.cs │ ├── DownloadProgressMonitor.cs │ ├── DownloadSpeedMonitor.cs │ ├── DownloadThrottling.cs │ └── DownloadToFileSaver.cs ├── Properties │ ├── AssemblyInfo.cs │ ├── Resources.Designer.cs │ ├── Resources.resx │ ├── Settings.Designer.cs │ └── Settings.settings └── Utils │ ├── DownloadChecker.cs │ ├── DownloadRangeHelper.cs │ └── SimpleWebRequestBuilder.cs ├── Downloader.Example ├── Downloader.Example.csproj ├── Program.cs └── Properties │ └── AssemblyInfo.cs ├── Downloader.Test ├── DownloadRangeHelperTest.cs ├── Downloader.Test.csproj ├── MultiPartDownloadTest.cs ├── Properties │ └── AssemblyInfo.cs ├── TestDownload.cs ├── TestDownloadBuilder.cs ├── TestDownloadChecker.cs ├── TestWebRequestBuilder.cs └── packages.config ├── Downloader.sln ├── GlobalAssemblyInfo.cs ├── LICENSE └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | 56 | # StyleCop 57 | StyleCopReport.xml 58 | 59 | # Files built by Visual Studio 60 | *_i.c 61 | *_p.c 62 | *_h.h 63 | *.ilk 64 | *.meta 65 | *.obj 66 | *.iobj 67 | *.pch 68 | *.pdb 69 | *.ipdb 70 | *.pgc 71 | *.pgd 72 | *.rsp 73 | *.sbr 74 | *.tlb 75 | *.tli 76 | *.tlh 77 | *.tmp 78 | *.tmp_proj 79 | *.log 80 | *.vspscc 81 | *.vssscc 82 | .builds 83 | *.pidb 84 | *.svclog 85 | *.scc 86 | 87 | # Chutzpah Test files 88 | _Chutzpah* 89 | 90 | # Visual C++ cache files 91 | ipch/ 92 | *.aps 93 | *.ncb 94 | *.opendb 95 | *.opensdf 96 | *.sdf 97 | *.cachefile 98 | *.VC.db 99 | *.VC.VC.opendb 100 | 101 | # Visual Studio profiler 102 | *.psess 103 | *.vsp 104 | *.vspx 105 | *.sap 106 | 107 | # Visual Studio Trace Files 108 | *.e2e 109 | 110 | # TFS 2012 Local Workspace 111 | $tf/ 112 | 113 | # Guidance Automation Toolkit 114 | *.gpState 115 | 116 | # ReSharper is a .NET coding add-in 117 | _ReSharper*/ 118 | *.[Rr]e[Ss]harper 119 | *.DotSettings.user 120 | 121 | # JustCode is a .NET coding add-in 122 | .JustCode 123 | 124 | # TeamCity is a build add-in 125 | _TeamCity* 126 | 127 | # DotCover is a Code Coverage Tool 128 | *.dotCover 129 | 130 | # AxoCover is a Code Coverage Tool 131 | .axoCover/* 132 | !.axoCover/settings.json 133 | 134 | # Visual Studio code coverage results 135 | *.coverage 136 | *.coveragexml 137 | 138 | # NCrunch 139 | _NCrunch_* 140 | .*crunch*.local.xml 141 | nCrunchTemp_* 142 | 143 | # MightyMoose 144 | *.mm.* 145 | AutoTest.Net/ 146 | 147 | # Web workbench (sass) 148 | .sass-cache/ 149 | 150 | # Installshield output folder 151 | [Ee]xpress/ 152 | 153 | # DocProject is a documentation generator add-in 154 | DocProject/buildhelp/ 155 | DocProject/Help/*.HxT 156 | DocProject/Help/*.HxC 157 | DocProject/Help/*.hhc 158 | DocProject/Help/*.hhk 159 | DocProject/Help/*.hhp 160 | DocProject/Help/Html2 161 | DocProject/Help/html 162 | 163 | # Click-Once directory 164 | publish/ 165 | 166 | # Publish Web Output 167 | *.[Pp]ublish.xml 168 | *.azurePubxml 169 | # Note: Comment the next line if you want to checkin your web deploy settings, 170 | # but database connection strings (with potential passwords) will be unencrypted 171 | *.pubxml 172 | *.publishproj 173 | 174 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 175 | # checkin your Azure Web App publish settings, but sensitive information contained 176 | # in these scripts will be unencrypted 177 | PublishScripts/ 178 | 179 | # NuGet Packages 180 | *.nupkg 181 | # The packages folder can be ignored because of Package Restore 182 | **/[Pp]ackages/* 183 | # except build/, which is used as an MSBuild target. 184 | !**/[Pp]ackages/build/ 185 | # Uncomment if necessary however generally it will be regenerated when needed 186 | #!**/[Pp]ackages/repositories.config 187 | # NuGet v3's project.json files produces more ignorable files 188 | *.nuget.props 189 | *.nuget.targets 190 | 191 | # Microsoft Azure Build Output 192 | csx/ 193 | *.build.csdef 194 | 195 | # Microsoft Azure Emulator 196 | ecf/ 197 | rcf/ 198 | 199 | # Windows Store app package directories and files 200 | AppPackages/ 201 | BundleArtifacts/ 202 | Package.StoreAssociation.xml 203 | _pkginfo.txt 204 | *.appx 205 | 206 | # Visual Studio cache files 207 | # files ending in .cache can be ignored 208 | *.[Cc]ache 209 | # but keep track of directories ending in .cache 210 | !*.[Cc]ache/ 211 | 212 | # Others 213 | ClientBin/ 214 | ~$* 215 | *~ 216 | *.dbmdl 217 | *.dbproj.schemaview 218 | *.jfm 219 | *.pfx 220 | *.publishsettings 221 | orleans.codegen.cs 222 | 223 | # Including strong name files can present a security risk 224 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 225 | #*.snk 226 | 227 | # Since there are multiple workflows, uncomment next line to ignore bower_components 228 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 229 | #bower_components/ 230 | 231 | # RIA/Silverlight projects 232 | Generated_Code/ 233 | 234 | # Backup & report files from converting an old project file 235 | # to a newer Visual Studio version. Backup files are not needed, 236 | # because we have git ;-) 237 | _UpgradeReport_Files/ 238 | Backup*/ 239 | UpgradeLog*.XML 240 | UpgradeLog*.htm 241 | ServiceFabricBackup/ 242 | *.rptproj.bak 243 | 244 | # SQL Server files 245 | *.mdf 246 | *.ldf 247 | *.ndf 248 | 249 | # Business Intelligence projects 250 | *.rdl.data 251 | *.bim.layout 252 | *.bim_*.settings 253 | *.rptproj.rsuser 254 | 255 | # Microsoft Fakes 256 | FakesAssemblies/ 257 | 258 | # GhostDoc plugin setting file 259 | *.GhostDoc.xml 260 | 261 | # Node.js Tools for Visual Studio 262 | .ntvs_analysis.dat 263 | node_modules/ 264 | 265 | # Visual Studio 6 build log 266 | *.plg 267 | 268 | # Visual Studio 6 workspace options file 269 | *.opt 270 | 271 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 272 | *.vbw 273 | 274 | # Visual Studio LightSwitch build output 275 | **/*.HTMLClient/GeneratedArtifacts 276 | **/*.DesktopClient/GeneratedArtifacts 277 | **/*.DesktopClient/ModelManifest.xml 278 | **/*.Server/GeneratedArtifacts 279 | **/*.Server/ModelManifest.xml 280 | _Pvt_Extensions 281 | 282 | # Paket dependency manager 283 | .paket/paket.exe 284 | paket-files/ 285 | 286 | # FAKE - F# Make 287 | .fake/ 288 | 289 | # JetBrains Rider 290 | .idea/ 291 | *.sln.iml 292 | 293 | # CodeRush 294 | .cr/ 295 | 296 | # Python Tools for Visual Studio (PTVS) 297 | __pycache__/ 298 | *.pyc 299 | 300 | # Cake - Uncomment if you are using it 301 | # tools/** 302 | # !tools/packages.config 303 | 304 | # Tabs Studio 305 | *.tss 306 | 307 | # Telerik's JustMock configuration file 308 | *.jmconfig 309 | 310 | # BizTalk build output 311 | *.btp.cs 312 | *.btm.cs 313 | *.odx.cs 314 | *.xsd.cs 315 | 316 | # OpenCover UI analysis results 317 | OpenCover/ 318 | 319 | # Azure Stream Analytics local run output 320 | ASALocalRun/ 321 | 322 | # MSBuild Binary and Structured Log 323 | *.binlog 324 | 325 | # NVidia Nsight GPU debugger configuration file 326 | *.nvuser 327 | 328 | # MFractors (Xamarin productivity tool) working folder 329 | .mfractor/ 330 | 331 | # Local History for Visual Studio 332 | .localhistory/ -------------------------------------------------------------------------------- /.nuget/NuGet.Config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Downloader.Business.Contract/DownloadCheckResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Toqe.Downloader.Business.Contract 6 | { 7 | public class DownloadCheckResult 8 | { 9 | public bool Success { get; set; } 10 | 11 | public long Size { get; set; } 12 | 13 | public int? StatusCode { get; set; } 14 | 15 | public bool SupportsResume { get; set; } 16 | 17 | public Exception Exception { get; set; } 18 | } 19 | } -------------------------------------------------------------------------------- /Downloader.Business.Contract/DownloadRange.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Toqe.Downloader.Business.Contract 6 | { 7 | public class DownloadRange 8 | { 9 | public DownloadRange() 10 | { 11 | } 12 | 13 | public DownloadRange(long start, long length) 14 | { 15 | this.Start = start; 16 | this.Length = length; 17 | } 18 | 19 | public long Start { get; set; } 20 | 21 | public long Length { get; set; } 22 | 23 | public long End 24 | { 25 | get { return this.Start + this.Length - 1; } 26 | } 27 | 28 | public override bool Equals(Object obj) 29 | { 30 | if (obj == null) 31 | { 32 | return false; 33 | } 34 | 35 | DownloadRange r = obj as DownloadRange; 36 | 37 | return Equals(r); 38 | } 39 | 40 | public bool Equals(DownloadRange r) 41 | { 42 | if ((Object)r == null) 43 | { 44 | return false; 45 | } 46 | 47 | return (this.Start == r.Start) && (this.Length == r.Length); 48 | } 49 | 50 | public override int GetHashCode() 51 | { 52 | return (int)(13 * this.Start * this.End); 53 | } 54 | 55 | public static bool operator ==(DownloadRange a, DownloadRange b) 56 | { 57 | if (System.Object.ReferenceEquals(a, b)) 58 | { 59 | return true; 60 | } 61 | 62 | if (((object)a == null) || ((object)b == null)) 63 | { 64 | return false; 65 | } 66 | 67 | return (object)a == null ? b.Equals(a) : a.Equals(b); 68 | } 69 | 70 | public static bool operator !=(DownloadRange a, DownloadRange b) 71 | { 72 | return !(a == b); 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /Downloader.Business.Contract/Downloader.Business.Contract.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {EDEBC948-AB61-479A-9AC5-99C2517DDB9D} 8 | Library 9 | Properties 10 | Toqe.Downloader.Business.Contract 11 | Toqe.Downloader.Business.Contract 12 | v3.5 13 | 512 14 | 15 | 16 | 17 | true 18 | full 19 | false 20 | ..\bin\ 21 | DEBUG;TRACE 22 | prompt 23 | 4 24 | 25 | 26 | pdbonly 27 | true 28 | ..\bin\ 29 | TRACE 30 | prompt 31 | 4 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | Properties\GlobalAssemblyInfo.cs 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 69 | -------------------------------------------------------------------------------- /Downloader.Business.Contract/Enums/DownloadState.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Toqe.Downloader.Business.Contract.Enums 6 | { 7 | public enum DownloadState 8 | { 9 | Undefined = 0, 10 | Initialized = 1, 11 | Running = 2, 12 | Finished = 3, 13 | Stopped = 4, 14 | Cancelled = 5 15 | } 16 | } -------------------------------------------------------------------------------- /Downloader.Business.Contract/Enums/DownloadStopType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace Toqe.Downloader.Business.Contract.Enums 7 | { 8 | public enum DownloadStopType 9 | { 10 | WithNotification, 11 | WithoutNotification 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Downloader.Business.Contract/Events/DownloadCancelledEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace Toqe.Downloader.Business.Contract.Events 7 | { 8 | public class DownloadCancelledEventArgs : DownloadEventArgs 9 | { 10 | public DownloadCancelledEventArgs() 11 | { 12 | } 13 | 14 | public DownloadCancelledEventArgs(IDownload download, Exception exception) 15 | { 16 | this.Download = download; 17 | this.Exception = exception; 18 | } 19 | 20 | public Exception Exception { get; set; } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Downloader.Business.Contract/Events/DownloadDataReceivedArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace Toqe.Downloader.Business.Contract.Events 7 | { 8 | public class DownloadDataReceivedEventArgs : DownloadEventArgs 9 | { 10 | public DownloadDataReceivedEventArgs() 11 | { 12 | } 13 | 14 | public DownloadDataReceivedEventArgs(IDownload download, byte[] data, long offset, int count) 15 | { 16 | this.Download = download; 17 | this.Data = data; 18 | this.Offset = offset; 19 | this.Count = count; 20 | } 21 | 22 | public byte[] Data { get; set; } 23 | 24 | public long Offset { get; set; } 25 | 26 | public int Count { get; set; } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Downloader.Business.Contract/Events/DownloadDelegates.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace Toqe.Downloader.Business.Contract.Events 7 | { 8 | public class DownloadDelegates 9 | { 10 | public delegate void DownloadDataReceivedHandler(DownloadDataReceivedEventArgs args); 11 | 12 | public delegate void DownloadStartedHandler(DownloadStartedEventArgs args); 13 | 14 | public delegate void DownloadCompletedHandler(DownloadEventArgs args); 15 | 16 | public delegate void DownloadStoppedHandler(DownloadEventArgs args); 17 | 18 | public delegate void DownloadCancelledHandler(DownloadCancelledEventArgs args); 19 | 20 | public delegate void VoidAction(); 21 | } 22 | } -------------------------------------------------------------------------------- /Downloader.Business.Contract/Events/DownloadEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace Toqe.Downloader.Business.Contract.Events 7 | { 8 | public class DownloadEventArgs 9 | { 10 | public IDownload Download { get; set; } 11 | 12 | public DownloadEventArgs() 13 | { 14 | } 15 | 16 | public DownloadEventArgs(IDownload download) 17 | { 18 | this.Download = download; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Downloader.Business.Contract/Events/DownloadStartedEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace Toqe.Downloader.Business.Contract.Events 7 | { 8 | public class DownloadStartedEventArgs : DownloadEventArgs 9 | { 10 | public DownloadStartedEventArgs() 11 | { 12 | } 13 | 14 | public DownloadStartedEventArgs(IDownload download, DownloadCheckResult checkResult, long alreadyDownloadedSize = 0) 15 | { 16 | this.Download = download; 17 | this.CheckResult = checkResult; 18 | this.AlreadyDownloadedSize = alreadyDownloadedSize; 19 | } 20 | 21 | public DownloadCheckResult CheckResult { get; set; } 22 | 23 | public long AlreadyDownloadedSize { get; set; } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Downloader.Business.Contract/Exceptions/DownloadCheckNotSuccessfulException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace Toqe.Downloader.Business.Contract.Exceptions 7 | { 8 | public class DownloadCheckNotSuccessfulException : Exception 9 | { 10 | public DownloadCheckNotSuccessfulException(string message, Exception ex, DownloadCheckResult downloadCheckResult) : base(message, ex) 11 | { 12 | this.DownloadCheckResult = downloadCheckResult; 13 | } 14 | 15 | public DownloadCheckResult DownloadCheckResult { get; private set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Downloader.Business.Contract/Exceptions/ResumingNotSupportedException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace Toqe.Downloader.Business.Contract.Exceptions 7 | { 8 | public class ResumingNotSupportedException : Exception 9 | { 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Downloader.Business.Contract/Exceptions/TooManyRetriesException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace Toqe.Downloader.Business.Contract.Exceptions 7 | { 8 | public class TooManyRetriesException : Exception 9 | { 10 | public TooManyRetriesException() 11 | : base() 12 | { 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Downloader.Business.Contract/IDownload.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using Toqe.Downloader.Business.Contract.Enums; 5 | using Toqe.Downloader.Business.Contract.Events; 6 | 7 | namespace Toqe.Downloader.Business.Contract 8 | { 9 | public interface IDownload : IDisposable 10 | { 11 | event DownloadDelegates.DownloadDataReceivedHandler DataReceived; 12 | 13 | event DownloadDelegates.DownloadStartedHandler DownloadStarted; 14 | 15 | event DownloadDelegates.DownloadCompletedHandler DownloadCompleted; 16 | 17 | event DownloadDelegates.DownloadStoppedHandler DownloadStopped; 18 | 19 | event DownloadDelegates.DownloadCancelledHandler DownloadCancelled; 20 | 21 | DownloadState State { get; } 22 | 23 | void Start(); 24 | 25 | void Stop(); 26 | 27 | void DetachAllHandlers(); 28 | } 29 | } -------------------------------------------------------------------------------- /Downloader.Business.Contract/IDownloadBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Text; 6 | 7 | namespace Toqe.Downloader.Business.Contract 8 | { 9 | public interface IDownloadBuilder 10 | { 11 | IDownload Build(Uri url, int bufferSize, long? offset, long? maxReadBytes); 12 | } 13 | } -------------------------------------------------------------------------------- /Downloader.Business.Contract/IDownloadChecker.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Text; 6 | 7 | namespace Toqe.Downloader.Business.Contract 8 | { 9 | public interface IDownloadChecker 10 | { 11 | DownloadCheckResult CheckDownload(WebResponse response); 12 | 13 | DownloadCheckResult CheckDownload(Uri url, IWebRequestBuilder requestBuilder); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Downloader.Business.Contract/IDownloadObserver.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace Toqe.Downloader.Business.Contract 7 | { 8 | public interface IDownloadObserver 9 | { 10 | void Attach(IDownload download); 11 | 12 | void Detach(IDownload download); 13 | 14 | void DetachAll(); 15 | } 16 | } -------------------------------------------------------------------------------- /Downloader.Business.Contract/IWebRequestBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Text; 6 | 7 | namespace Toqe.Downloader.Business.Contract 8 | { 9 | public interface IWebRequestBuilder 10 | { 11 | HttpWebRequest CreateRequest(Uri url, long? offset); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Downloader.Business.Contract/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // Allgemeine Informationen über eine Assembly werden über die folgenden 6 | // Attribute gesteuert. Ändern Sie diese Attributwerte, um die Informationen zu ändern, 7 | // die mit einer Assembly verknüpft sind. 8 | [assembly: AssemblyTitle("Downloader.Business.Contract")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyCulture("")] 11 | 12 | // Die folgende GUID bestimmt die ID der Typbibliothek, wenn dieses Projekt für COM verfügbar gemacht wird 13 | [assembly: Guid("868fadc7-4749-4159-b5e5-03a3e2a3d61d")] 14 | -------------------------------------------------------------------------------- /Downloader.Business/Download/AbstractDownload.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net; 4 | using System.Text; 5 | using System.Threading; 6 | using Toqe.Downloader.Business.Contract; 7 | using Toqe.Downloader.Business.Contract.Enums; 8 | using Toqe.Downloader.Business.Contract.Events; 9 | 10 | namespace Toqe.Downloader.Business.Download 11 | { 12 | public abstract class AbstractDownload : IDownload 13 | { 14 | public event DownloadDelegates.DownloadDataReceivedHandler DataReceived; 15 | 16 | public event DownloadDelegates.DownloadStartedHandler DownloadStarted; 17 | 18 | public event DownloadDelegates.DownloadCompletedHandler DownloadCompleted; 19 | 20 | public event DownloadDelegates.DownloadStoppedHandler DownloadStopped; 21 | 22 | public event DownloadDelegates.DownloadCancelledHandler DownloadCancelled; 23 | 24 | protected DownloadState state = DownloadState.Undefined; 25 | 26 | protected Uri url; 27 | 28 | protected int bufferSize; 29 | 30 | protected long? offset; 31 | 32 | protected long? maxReadBytes; 33 | 34 | protected IWebRequestBuilder requestBuilder; 35 | 36 | protected IDownloadChecker downloadChecker; 37 | 38 | protected bool stopping = false; 39 | 40 | protected readonly object monitor = new object(); 41 | 42 | public AbstractDownload(Uri url, int bufferSize, long? offset, long? maxReadBytes, IWebRequestBuilder requestBuilder, IDownloadChecker downloadChecker) 43 | { 44 | if (url == null) 45 | throw new ArgumentNullException("url"); 46 | 47 | if (bufferSize < 0) 48 | throw new ArgumentException("bufferSize < 0"); 49 | 50 | if (offset.HasValue && offset.Value < 0) 51 | throw new ArgumentException("offset < 0"); 52 | 53 | if (maxReadBytes.HasValue && maxReadBytes.Value < 0) 54 | throw new ArgumentException("maxReadBytes < 0"); 55 | 56 | this.url = url; 57 | this.bufferSize = bufferSize; 58 | this.offset = offset; 59 | this.maxReadBytes = maxReadBytes; 60 | this.requestBuilder = requestBuilder; 61 | this.downloadChecker = downloadChecker; 62 | 63 | this.state = DownloadState.Initialized; 64 | } 65 | 66 | public DownloadState State 67 | { 68 | get { return this.state; } 69 | } 70 | 71 | public virtual void Start() 72 | { 73 | lock (this.monitor) 74 | { 75 | if (this.state != DownloadState.Initialized) 76 | { 77 | throw new InvalidOperationException("Invalid state: " + this.state); 78 | } 79 | 80 | this.state = DownloadState.Running; 81 | } 82 | 83 | this.OnStart(); 84 | } 85 | 86 | public virtual void Stop() 87 | { 88 | this.DoStop(DownloadStopType.WithNotification); 89 | } 90 | 91 | protected virtual void DoStop(DownloadStopType stopType) 92 | { 93 | lock (this.monitor) 94 | { 95 | this.stopping = true; 96 | } 97 | 98 | this.OnStop(); 99 | 100 | if (stopType == DownloadStopType.WithNotification) 101 | { 102 | this.OnDownloadStopped(new DownloadEventArgs(this)); 103 | } 104 | } 105 | 106 | public virtual void Dispose() 107 | { 108 | this.Stop(); 109 | } 110 | 111 | public virtual void DetachAllHandlers() 112 | { 113 | if (this.DataReceived != null) 114 | { 115 | foreach (var i in this.DataReceived.GetInvocationList()) 116 | { 117 | this.DataReceived -= (DownloadDelegates.DownloadDataReceivedHandler)i; 118 | } 119 | } 120 | 121 | if (this.DownloadCancelled != null) 122 | { 123 | foreach (var i in this.DownloadCancelled.GetInvocationList()) 124 | { 125 | this.DownloadCancelled -= (DownloadDelegates.DownloadCancelledHandler)i; 126 | } 127 | } 128 | 129 | if (this.DownloadCompleted != null) 130 | { 131 | foreach (var i in this.DownloadCompleted.GetInvocationList()) 132 | { 133 | this.DownloadCompleted -= (DownloadDelegates.DownloadCompletedHandler)i; 134 | } 135 | } 136 | 137 | if (this.DownloadStopped != null) 138 | { 139 | foreach (var i in this.DownloadStopped.GetInvocationList()) 140 | { 141 | this.DownloadStopped -= (DownloadDelegates.DownloadStoppedHandler)i; 142 | } 143 | } 144 | 145 | if (this.DownloadStarted != null) 146 | { 147 | foreach (var i in this.DownloadStarted.GetInvocationList()) 148 | { 149 | this.DownloadStarted -= (DownloadDelegates.DownloadStartedHandler)i; 150 | } 151 | } 152 | } 153 | 154 | protected virtual void OnStart() 155 | { 156 | // Implementations should start their work here. 157 | } 158 | 159 | protected virtual void OnStop() 160 | { 161 | // This happens, when the Stop method is called. 162 | // Implementations should clean up and free their ressources here. 163 | // The stop event must not be triggered in here, it is triggered in the context of the Stop method. 164 | } 165 | 166 | protected virtual void StartThread(DownloadDelegates.VoidAction func, string name) 167 | { 168 | var thread = new Thread(new ThreadStart(func)) { Name = name }; 169 | thread.Start(); 170 | } 171 | 172 | protected virtual void OnDataReceived(DownloadDataReceivedEventArgs args) 173 | { 174 | if (this.DataReceived != null) 175 | { 176 | this.DataReceived(args); 177 | } 178 | } 179 | 180 | protected virtual void OnDownloadStarted(DownloadStartedEventArgs args) 181 | { 182 | if (this.DownloadStarted != null) 183 | { 184 | this.DownloadStarted(args); 185 | } 186 | } 187 | 188 | protected virtual void OnDownloadCompleted(DownloadEventArgs args) 189 | { 190 | if (this.DownloadCompleted != null) 191 | { 192 | this.DownloadCompleted(args); 193 | } 194 | } 195 | 196 | protected virtual void OnDownloadStopped(DownloadEventArgs args) 197 | { 198 | if (this.DownloadStopped != null) 199 | { 200 | this.DownloadStopped(args); 201 | } 202 | } 203 | 204 | protected virtual void OnDownloadCancelled(DownloadCancelledEventArgs args) 205 | { 206 | if (this.DownloadCancelled != null) 207 | { 208 | this.DownloadCancelled(args); 209 | } 210 | } 211 | } 212 | } -------------------------------------------------------------------------------- /Downloader.Business/Download/MultiPartDownload.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Text; 6 | using System.Threading; 7 | using Toqe.Downloader.Business.Contract; 8 | using Toqe.Downloader.Business.Contract.Enums; 9 | using Toqe.Downloader.Business.Contract.Events; 10 | using Toqe.Downloader.Business.Contract.Exceptions; 11 | using Toqe.Downloader.Business.Utils; 12 | 13 | namespace Toqe.Downloader.Business.Download 14 | { 15 | public class MultiPartDownload : AbstractDownload 16 | { 17 | private readonly DownloadRangeHelper downloadRangeHelper = new DownloadRangeHelper(); 18 | 19 | private readonly int numberOfParts; 20 | 21 | private readonly IDownloadBuilder downloadBuilder; 22 | 23 | private readonly Dictionary downloads = new Dictionary(); 24 | 25 | public MultiPartDownload( 26 | Uri url, 27 | int bufferSize, 28 | int numberOfParts, 29 | IDownloadBuilder downloadBuilder, 30 | IWebRequestBuilder requestBuilder, 31 | IDownloadChecker downloadChecker, 32 | List alreadyDownloadedRanges) 33 | : base(url, bufferSize, null, null, requestBuilder, downloadChecker) 34 | { 35 | if (numberOfParts <= 0) 36 | throw new ArgumentException("numberOfParts <= 0"); 37 | 38 | if (downloadBuilder == null) 39 | throw new ArgumentNullException("downloadBuilder"); 40 | 41 | this.numberOfParts = numberOfParts; 42 | this.downloadBuilder = downloadBuilder; 43 | this.AlreadyDownloadedRanges = alreadyDownloadedRanges ?? new List(); 44 | 45 | if (System.Net.ServicePointManager.DefaultConnectionLimit < numberOfParts) 46 | { 47 | System.Net.ServicePointManager.DefaultConnectionLimit = numberOfParts; 48 | } 49 | } 50 | 51 | public List AlreadyDownloadedRanges { get; private set; } 52 | 53 | public List ToDoRanges { get; private set; } 54 | 55 | protected override void OnStart() 56 | { 57 | var downloadCheck = this.PerformInitialDownloadCheck(); 58 | this.DetermineFileSizeAndStartDownloads(downloadCheck); 59 | } 60 | 61 | protected override void OnStop() 62 | { 63 | List currentDownloads = new List(); 64 | 65 | lock (this.monitor) 66 | { 67 | if (this.downloads != null && this.downloads.Count > 0) 68 | { 69 | currentDownloads = new List(this.downloads.Keys); 70 | } 71 | } 72 | 73 | foreach (var download in currentDownloads) 74 | { 75 | download.DetachAllHandlers(); 76 | download.Stop(); 77 | } 78 | 79 | lock (this.monitor) 80 | { 81 | this.state = DownloadState.Stopped; 82 | } 83 | } 84 | 85 | private DownloadCheckResult PerformInitialDownloadCheck() 86 | { 87 | var downloadCheck = this.downloadChecker.CheckDownload(this.url, this.requestBuilder); 88 | 89 | if (!downloadCheck.Success) 90 | throw new DownloadCheckNotSuccessfulException("Download check was not successful. HTTP status code: " + downloadCheck.StatusCode, downloadCheck.Exception, downloadCheck); 91 | 92 | if (!downloadCheck.SupportsResume) 93 | throw new ResumingNotSupportedException(); 94 | 95 | this.OnDownloadStarted(new DownloadStartedEventArgs(this, downloadCheck, this.AlreadyDownloadedRanges.Sum(x => x.Length))); 96 | 97 | return downloadCheck; 98 | } 99 | 100 | private void DetermineFileSizeAndStartDownloads(DownloadCheckResult downloadCheck) 101 | { 102 | lock (this.monitor) 103 | { 104 | if (this.AlreadyDownloadedRanges.Count == 1 && this.AlreadyDownloadedRanges[0].Length == downloadCheck.Size) 105 | { 106 | this.state = DownloadState.Finished; 107 | OnDownloadCompleted(new DownloadEventArgs(this)); 108 | return; 109 | } 110 | 111 | this.ToDoRanges = this.DetermineToDoRanges(downloadCheck.Size, this.AlreadyDownloadedRanges); 112 | this.SplitToDoRangesForNumberOfParts(); 113 | 114 | for (int i = 0; i < this.ToDoRanges.Count; i++) 115 | { 116 | var todoRange = this.ToDoRanges[i]; 117 | StartDownload(todoRange); 118 | } 119 | } 120 | } 121 | 122 | private void SplitToDoRangesForNumberOfParts() 123 | { 124 | while (this.ToDoRanges.Count < this.numberOfParts) 125 | { 126 | var maxRange = this.ToDoRanges.FirstOrDefault(r => r.Length == this.ToDoRanges.Max(r2 => r2.Length)); 127 | if (maxRange == null) 128 | { 129 | return; 130 | } 131 | 132 | this.ToDoRanges.Remove(maxRange); 133 | var range1Start = maxRange.Start; 134 | var range1Length = maxRange.Length / 2; 135 | var range2Start = range1Start + range1Length; 136 | var range2Length = maxRange.End - range2Start + 1; 137 | this.ToDoRanges.Add(new DownloadRange(range1Start, range1Length)); 138 | this.ToDoRanges.Add(new DownloadRange(range2Start, range2Length)); 139 | } 140 | } 141 | 142 | private void StartDownload(DownloadRange range) 143 | { 144 | var download = this.downloadBuilder.Build(this.url, this.bufferSize, range.Start, range.Length); 145 | download.DataReceived += downloadDataReceived; 146 | download.DownloadCancelled += downloadCancelled; 147 | download.DownloadCompleted += downloadCompleted; 148 | download.Start(); 149 | 150 | lock (this.monitor) 151 | { 152 | this.downloads.Add(download, range); 153 | } 154 | } 155 | 156 | private List DetermineToDoRanges(long fileSize, List alreadyDoneRanges) 157 | { 158 | var result = new List(); 159 | 160 | var initialRange = new DownloadRange(0, fileSize); 161 | result.Add(initialRange); 162 | 163 | if (alreadyDoneRanges != null && alreadyDoneRanges.Count > 0) 164 | { 165 | foreach (var range in alreadyDoneRanges) 166 | { 167 | var newResult = new List(result); 168 | 169 | foreach (var resultRange in result) 170 | { 171 | if (this.downloadRangeHelper.RangesCollide(range, resultRange)) 172 | { 173 | newResult.Remove(resultRange); 174 | var difference = this.downloadRangeHelper.RangeDifference(resultRange, range); 175 | newResult.AddRange(difference); 176 | } 177 | } 178 | 179 | result = newResult; 180 | } 181 | } 182 | 183 | return result; 184 | } 185 | 186 | private void StartDownloadOfNextRange() 187 | { 188 | DownloadRange nextRange = null; 189 | 190 | lock (this.monitor) 191 | { 192 | nextRange = this.ToDoRanges.FirstOrDefault(r => !this.downloads.Values.Any(r2 => downloadRangeHelper.RangesCollide(r, r2))); 193 | } 194 | 195 | if (nextRange != null) 196 | { 197 | StartDownload(nextRange); 198 | } 199 | 200 | if (!this.downloads.Any()) 201 | { 202 | lock (this.monitor) 203 | { 204 | this.state = DownloadState.Finished; 205 | } 206 | 207 | this.OnDownloadCompleted(new DownloadEventArgs(this)); 208 | } 209 | } 210 | 211 | private void downloadDataReceived(DownloadDataReceivedEventArgs args) 212 | { 213 | var offset = args.Offset; 214 | var count = args.Count; 215 | var data = args.Data; 216 | 217 | lock (this.monitor) 218 | { 219 | var justDownloadedRange = new DownloadRange(offset, count); 220 | 221 | var todoRange = ToDoRanges.FirstOrDefault(r => downloadRangeHelper.RangesCollide(r, justDownloadedRange)); 222 | 223 | if (todoRange != null) 224 | { 225 | ToDoRanges.Remove(todoRange); 226 | var differences = downloadRangeHelper.RangeDifference(todoRange, justDownloadedRange); 227 | ToDoRanges.AddRange(differences); 228 | } 229 | 230 | var alreadyDoneRange = this.AlreadyDownloadedRanges.FirstOrDefault(r => r.End + 1 == justDownloadedRange.Start); 231 | 232 | if (alreadyDoneRange == null) 233 | { 234 | alreadyDoneRange = justDownloadedRange; 235 | this.AlreadyDownloadedRanges.Add(alreadyDoneRange); 236 | } 237 | else 238 | { 239 | alreadyDoneRange.Length += justDownloadedRange.Length; 240 | } 241 | 242 | var neighborRange = this.AlreadyDownloadedRanges.FirstOrDefault(r => r.Start == alreadyDoneRange.End + 1); 243 | 244 | if (neighborRange != null) 245 | { 246 | this.AlreadyDownloadedRanges.Remove(alreadyDoneRange); 247 | this.AlreadyDownloadedRanges.Remove(neighborRange); 248 | var combinedRange = new DownloadRange(alreadyDoneRange.Start, alreadyDoneRange.Length + neighborRange.Length); 249 | this.AlreadyDownloadedRanges.Add(combinedRange); 250 | } 251 | } 252 | 253 | this.OnDataReceived(new DownloadDataReceivedEventArgs(this, data, offset, count)); 254 | } 255 | 256 | private void downloadCompleted(DownloadEventArgs args) 257 | { 258 | lock (this.monitor) 259 | { 260 | var resumingDownload = (ResumingDownload)args.Download; 261 | this.downloads.Remove(resumingDownload); 262 | } 263 | 264 | this.StartDownloadOfNextRange(); 265 | } 266 | 267 | private void downloadCancelled(DownloadCancelledEventArgs args) 268 | { 269 | this.StartDownloadOfNextRange(); 270 | } 271 | } 272 | } -------------------------------------------------------------------------------- /Downloader.Business/Download/ResumingDownload.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Net; 5 | using System.Text; 6 | using System.Threading; 7 | using Toqe.Downloader.Business.Contract; 8 | using Toqe.Downloader.Business.Contract.Enums; 9 | using Toqe.Downloader.Business.Contract.Events; 10 | using Toqe.Downloader.Business.Contract.Exceptions; 11 | 12 | namespace Toqe.Downloader.Business.Download 13 | { 14 | public class ResumingDownload : AbstractDownload 15 | { 16 | private readonly int timeForHeartbeat; 17 | 18 | private readonly int timeToRetry; 19 | 20 | private readonly int? maxRetries; 21 | 22 | private readonly IDownloadBuilder downloadBuilder; 23 | 24 | private bool downloadStartedNotified; 25 | 26 | private long currentOffset; 27 | 28 | private long sumOfBytesRead; 29 | 30 | private IDownload currentDownload; 31 | 32 | private DateTime lastHeartbeat; 33 | 34 | private int currentRetry = 0; 35 | 36 | public ResumingDownload(Uri url, int bufferSize, long? offset, long? maxReadBytes, int timeForHeartbeat, int timeToRetry, int? maxRetries, IDownloadBuilder downloadBuilder) 37 | : base(url, bufferSize, offset, maxReadBytes, null, null) 38 | { 39 | if (timeForHeartbeat <= 0) 40 | throw new ArgumentException("timeForHeartbeat <= 0"); 41 | 42 | if (timeToRetry <= 0) 43 | throw new ArgumentException("timeToRetry <= 0"); 44 | 45 | if (downloadBuilder == null) 46 | throw new ArgumentException("downloadBuilder"); 47 | 48 | this.timeForHeartbeat = timeForHeartbeat; 49 | this.timeToRetry = timeToRetry; 50 | this.maxRetries = maxRetries; 51 | this.downloadBuilder = downloadBuilder; 52 | } 53 | 54 | protected override void OnStart() 55 | { 56 | StartThread(this.StartDownload, string.Format("ResumingDownload offset {0} length {1} Main", this.offset, this.maxReadBytes)); 57 | StartThread(this.CheckHeartbeat, string.Format("ResumingDownload offset {0} length {1} Heartbeat", this.offset, this.maxReadBytes)); 58 | } 59 | 60 | protected override void OnStop() 61 | { 62 | lock (this.monitor) 63 | { 64 | this.stopping = true; 65 | this.DoStopIfNecessary(); 66 | } 67 | } 68 | 69 | private void StartDownload() 70 | { 71 | lock (this.monitor) 72 | { 73 | StartNewDownload(); 74 | } 75 | } 76 | 77 | private void StartNewDownload() 78 | { 79 | this.currentOffset = this.offset.HasValue ? this.offset.Value : 0; 80 | BuildDownload(); 81 | } 82 | 83 | private void CheckHeartbeat() 84 | { 85 | while (true) 86 | { 87 | Thread.Sleep(this.timeForHeartbeat); 88 | 89 | lock (this.monitor) 90 | { 91 | if (this.DoStopIfNecessary()) 92 | { 93 | return; 94 | } 95 | 96 | if (DateTime.Now - this.lastHeartbeat > TimeSpan.FromMilliseconds(this.timeForHeartbeat)) 97 | { 98 | CountRetryAndCancelIfMaxRetriesReached(); 99 | 100 | if (this.currentDownload != null) 101 | { 102 | this.CloseDownload(); 103 | StartThread(this.BuildDownload, Thread.CurrentThread.Name + "-byHeartbeat"); 104 | } 105 | } 106 | } 107 | } 108 | } 109 | 110 | private void CountRetryAndCancelIfMaxRetriesReached() 111 | { 112 | if (this.maxRetries.HasValue && this.currentRetry >= this.maxRetries) 113 | { 114 | this.state = DownloadState.Cancelled; 115 | this.OnDownloadCancelled(new DownloadCancelledEventArgs(this, new TooManyRetriesException())); 116 | this.DoStop(DownloadStopType.WithoutNotification); 117 | } 118 | 119 | this.currentRetry++; 120 | } 121 | 122 | private void BuildDownload() 123 | { 124 | lock (this.monitor) 125 | { 126 | if (this.DoStopIfNecessary()) 127 | { 128 | return; 129 | } 130 | 131 | long? currentMaxReadBytes = this.maxReadBytes.HasValue ? (long?)this.maxReadBytes.Value - this.sumOfBytesRead : null; 132 | 133 | this.currentDownload = this.downloadBuilder.Build(this.url, this.bufferSize, this.currentOffset, currentMaxReadBytes); 134 | this.currentDownload.DownloadStarted += downloadStarted; 135 | this.currentDownload.DownloadCancelled += downloadCancelled; 136 | this.currentDownload.DownloadCompleted += downloadCompleted; 137 | this.currentDownload.DataReceived += downloadDataReceived; 138 | StartThread(this.currentDownload.Start, Thread.CurrentThread.Name + "-buildDownload"); 139 | } 140 | } 141 | 142 | private bool DoStopIfNecessary() 143 | { 144 | if (this.stopping) 145 | { 146 | this.CloseDownload(); 147 | 148 | lock (this.monitor) 149 | { 150 | this.state = DownloadState.Stopped; 151 | } 152 | } 153 | 154 | return this.stopping; 155 | } 156 | 157 | private void SleepThenBuildDownload() 158 | { 159 | Thread.Sleep(this.timeToRetry); 160 | BuildDownload(); 161 | } 162 | 163 | private void CloseDownload() 164 | { 165 | if (this.currentDownload != null) 166 | { 167 | this.currentDownload.DetachAllHandlers(); 168 | this.currentDownload.Stop(); 169 | this.currentDownload = null; 170 | } 171 | } 172 | 173 | private void downloadDataReceived(DownloadDataReceivedEventArgs args) 174 | { 175 | var download = args.Download; 176 | var count = args.Count; 177 | var data = args.Data; 178 | long previousOffset = 0; 179 | 180 | lock (this.monitor) 181 | { 182 | if (this.currentDownload == download) 183 | { 184 | if (this.DoStopIfNecessary()) 185 | { 186 | return; 187 | } 188 | 189 | previousOffset = this.currentOffset; 190 | 191 | this.lastHeartbeat = DateTime.Now; 192 | this.currentOffset += count; 193 | this.sumOfBytesRead += count; 194 | } 195 | } 196 | 197 | this.OnDataReceived(new DownloadDataReceivedEventArgs(this, data, previousOffset, count)); 198 | } 199 | 200 | private void downloadStarted(DownloadStartedEventArgs args) 201 | { 202 | var download = args.Download; 203 | bool shouldNotifyDownloadStarted = false; 204 | 205 | lock (this.monitor) 206 | { 207 | if (download == this.currentDownload) 208 | { 209 | if (!this.downloadStartedNotified) 210 | { 211 | shouldNotifyDownloadStarted = true; 212 | this.downloadStartedNotified = true; 213 | } 214 | } 215 | } 216 | 217 | if (shouldNotifyDownloadStarted) 218 | { 219 | this.OnDownloadStarted(new DownloadStartedEventArgs(this, args.CheckResult, args.AlreadyDownloadedSize)); 220 | } 221 | } 222 | 223 | private void downloadCompleted(DownloadEventArgs args) 224 | { 225 | lock (this.monitor) 226 | { 227 | this.CloseDownload(); 228 | this.state = DownloadState.Finished; 229 | this.stopping = true; 230 | } 231 | 232 | this.OnDownloadCompleted(new DownloadEventArgs(this)); 233 | } 234 | 235 | private void downloadCancelled(DownloadCancelledEventArgs args) 236 | { 237 | var download = args.Download; 238 | 239 | lock (this.monitor) 240 | { 241 | if (download == this.currentDownload) 242 | { 243 | CountRetryAndCancelIfMaxRetriesReached(); 244 | 245 | if (this.currentDownload != null) 246 | { 247 | this.currentDownload = null; 248 | StartThread(this.SleepThenBuildDownload, Thread.CurrentThread.Name + "-afterCancel"); 249 | } 250 | } 251 | } 252 | } 253 | } 254 | } -------------------------------------------------------------------------------- /Downloader.Business/Download/SimpleDownload.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Net; 5 | using System.Text; 6 | using Toqe.Downloader.Business.Contract; 7 | using Toqe.Downloader.Business.Contract.Enums; 8 | using Toqe.Downloader.Business.Contract.Events; 9 | 10 | namespace Toqe.Downloader.Business.Download 11 | { 12 | public class SimpleDownload : AbstractDownload 13 | { 14 | public SimpleDownload(Uri url, int bufferSize, long? offset, long? maxReadBytes, IWebRequestBuilder requestBuilder, IDownloadChecker downloadChecker) 15 | : base(url, bufferSize, offset, maxReadBytes, requestBuilder, downloadChecker) 16 | { 17 | } 18 | 19 | protected override void OnStart() 20 | { 21 | try 22 | { 23 | var request = this.requestBuilder.CreateRequest(this.url, this.offset); 24 | 25 | using (var response = request.GetResponse()) 26 | { 27 | var httpResponse = response as HttpWebResponse; 28 | 29 | if (httpResponse != null) 30 | { 31 | var statusCode = httpResponse.StatusCode; 32 | 33 | if (!(statusCode == HttpStatusCode.OK || (this.offset.HasValue && statusCode == HttpStatusCode.PartialContent))) 34 | { 35 | throw new InvalidOperationException("Invalid HTTP status code: " + httpResponse.StatusCode); 36 | } 37 | } 38 | 39 | var checkResult = this.downloadChecker.CheckDownload(response); 40 | var supportsResume = checkResult.SupportsResume; 41 | long currentOffset = supportsResume && this.offset.HasValue ? this.offset.Value : 0; 42 | long sumOfBytesRead = 0; 43 | 44 | this.OnDownloadStarted(new DownloadStartedEventArgs(this, checkResult, currentOffset)); 45 | 46 | using (var stream = response.GetResponseStream()) 47 | { 48 | byte[] buffer = new byte[this.bufferSize]; 49 | 50 | while (true) 51 | { 52 | lock (this.monitor) 53 | { 54 | if (this.stopping) 55 | { 56 | this.state = DownloadState.Stopped; 57 | break; 58 | } 59 | } 60 | 61 | int bytesRead = stream.Read(buffer, 0, buffer.Length); 62 | 63 | if (bytesRead == 0) 64 | { 65 | lock (this.monitor) 66 | { 67 | this.state = DownloadState.Finished; 68 | } 69 | 70 | this.OnDownloadCompleted(new DownloadEventArgs(this)); 71 | break; 72 | } 73 | 74 | if (maxReadBytes.HasValue && sumOfBytesRead + bytesRead > maxReadBytes.Value) 75 | { 76 | var count = (int)(maxReadBytes.Value - sumOfBytesRead); 77 | 78 | if (count > 0) 79 | { 80 | this.OnDataReceived(new DownloadDataReceivedEventArgs(this, buffer, currentOffset, count)); 81 | } 82 | } 83 | else 84 | { 85 | this.OnDataReceived(new DownloadDataReceivedEventArgs(this, buffer, currentOffset, bytesRead)); 86 | } 87 | 88 | currentOffset += bytesRead; 89 | sumOfBytesRead += bytesRead; 90 | 91 | if (this.maxReadBytes.HasValue && sumOfBytesRead >= this.maxReadBytes.Value) 92 | { 93 | lock (this.monitor) 94 | { 95 | this.state = DownloadState.Finished; 96 | } 97 | 98 | this.OnDownloadCompleted(new DownloadEventArgs(this)); 99 | break; 100 | } 101 | } 102 | } 103 | } 104 | } 105 | catch (Exception ex) 106 | { 107 | lock (this.monitor) 108 | { 109 | this.state = DownloadState.Cancelled; 110 | } 111 | 112 | this.OnDownloadCancelled(new DownloadCancelledEventArgs(this, ex)); 113 | } 114 | } 115 | } 116 | } -------------------------------------------------------------------------------- /Downloader.Business/DownloadBuilder/MultiPartDownloadBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using Toqe.Downloader.Business.Contract; 6 | using Toqe.Downloader.Business.Download; 7 | 8 | namespace Toqe.Downloader.Business.DownloadBuilder 9 | { 10 | public class MultiPartDownloadBuilder : IDownloadBuilder 11 | { 12 | private readonly int numberOfParts; 13 | 14 | private readonly IDownloadBuilder downloadBuilder; 15 | 16 | private readonly IWebRequestBuilder requestBuilder; 17 | 18 | private readonly IDownloadChecker downloadChecker; 19 | 20 | private readonly List alreadyDownloadedRanges; 21 | 22 | public MultiPartDownloadBuilder( 23 | int numberOfParts, 24 | IDownloadBuilder downloadBuilder, 25 | IWebRequestBuilder requestBuilder, 26 | IDownloadChecker downloadChecker, 27 | List alreadyDownloadedRanges) 28 | { 29 | if (numberOfParts <= 0) 30 | throw new ArgumentException("numberOfParts <= 0"); 31 | 32 | if (downloadBuilder == null) 33 | throw new ArgumentNullException("downloadBuilder"); 34 | 35 | if (requestBuilder == null) 36 | throw new ArgumentNullException("requestBuilder"); 37 | 38 | if (downloadChecker == null) 39 | throw new ArgumentNullException("downloadChecker"); 40 | 41 | this.numberOfParts = numberOfParts; 42 | this.downloadBuilder = downloadBuilder; 43 | this.requestBuilder = requestBuilder; 44 | this.downloadChecker = downloadChecker; 45 | this.alreadyDownloadedRanges = alreadyDownloadedRanges ?? new List(); 46 | } 47 | 48 | public IDownload Build(Uri url, int bufferSize, long? offset, long? maxReadBytes) 49 | { 50 | return new MultiPartDownload(url, bufferSize, this.numberOfParts, this.downloadBuilder, this.requestBuilder, this.downloadChecker, this.alreadyDownloadedRanges); 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /Downloader.Business/DownloadBuilder/ResumingDownloadBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Text; 6 | using Toqe.Downloader.Business.Contract; 7 | using Toqe.Downloader.Business.Download; 8 | 9 | namespace Toqe.Downloader.Business.DownloadBuilder 10 | { 11 | public class ResumingDownloadBuilder : IDownloadBuilder 12 | { 13 | private readonly int timeForHeartbeat; 14 | 15 | private readonly int timeToRetry; 16 | 17 | private readonly int? maxRetries; 18 | 19 | private readonly IDownloadBuilder downloadBuilder; 20 | 21 | public ResumingDownloadBuilder(int timeForHeartbeat, int timeToRetry, int? maxRetries, IDownloadBuilder downloadBuilder) 22 | { 23 | if (timeForHeartbeat <= 0) 24 | throw new ArgumentException("timeForHeartbeat <= 0"); 25 | 26 | if (timeToRetry <= 0) 27 | throw new ArgumentException("timeToRetry <= 0"); 28 | 29 | if (downloadBuilder == null) 30 | throw new ArgumentNullException("downloadBuilder"); 31 | 32 | this.timeForHeartbeat = timeForHeartbeat; 33 | this.timeToRetry = timeToRetry; 34 | this.maxRetries = maxRetries; 35 | this.downloadBuilder = downloadBuilder; 36 | } 37 | 38 | public IDownload Build(Uri url, int bufferSize, long? offset, long? maxReadBytes) 39 | { 40 | return new ResumingDownload(url, bufferSize, offset, maxReadBytes, this.timeForHeartbeat, this.timeToRetry, this.maxRetries, this.downloadBuilder); 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /Downloader.Business/DownloadBuilder/SimpleDownloadBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using Toqe.Downloader.Business.Contract; 6 | using Toqe.Downloader.Business.Download; 7 | 8 | namespace Toqe.Downloader.Business.DownloadBuilder 9 | { 10 | public class SimpleDownloadBuilder : IDownloadBuilder 11 | { 12 | private readonly IWebRequestBuilder requestBuilder; 13 | 14 | private readonly IDownloadChecker downloadChecker; 15 | 16 | public SimpleDownloadBuilder(IWebRequestBuilder requestBuilder, IDownloadChecker downloadChecker) 17 | { 18 | if (requestBuilder == null) 19 | throw new ArgumentNullException("requestBuilder"); 20 | 21 | if (downloadChecker == null) 22 | throw new ArgumentNullException("downloadChecker"); 23 | 24 | this.requestBuilder = requestBuilder; 25 | this.downloadChecker = downloadChecker; 26 | } 27 | 28 | public IDownload Build(Uri url, int bufferSize, long? offset, long? maxReadBytes) 29 | { 30 | return new SimpleDownload(url, bufferSize, offset, maxReadBytes, this.requestBuilder, this.downloadChecker); 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /Downloader.Business/Downloader.Business.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {045CE992-F761-4541-BE07-D9DA5B1E37C5} 8 | Library 9 | Properties 10 | Toqe.Downloader.Business 11 | Toqe.Downloader.Business 12 | v3.5 13 | 512 14 | 15 | 16 | 17 | AnyCPU 18 | true 19 | full 20 | false 21 | ..\bin\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | 26 | 27 | AnyCPU 28 | pdbonly 29 | true 30 | ..\bin\ 31 | TRACE 32 | prompt 33 | 4 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | Properties\GlobalAssemblyInfo.cs 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | ResXFileCodeGenerator 69 | Resources.Designer.cs 70 | Designer 71 | 72 | 73 | True 74 | Resources.resx 75 | True 76 | 77 | 78 | SettingsSingleFileGenerator 79 | Settings.Designer.cs 80 | 81 | 82 | True 83 | Settings.settings 84 | True 85 | 86 | 87 | 88 | 89 | {edebc948-ab61-479a-9ac5-99c2517ddb9d} 90 | Downloader.Business.Contract 91 | 92 | 93 | 94 | 101 | -------------------------------------------------------------------------------- /Downloader.Business/Observer/AbstractDownloadObserver.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using Toqe.Downloader.Business.Contract; 6 | 7 | namespace Toqe.Downloader.Business.Observer 8 | { 9 | public abstract class AbstractDownloadObserver : IDownloadObserver, IDisposable 10 | { 11 | protected List attachedDownloads = new List(); 12 | 13 | protected object monitor = new object(); 14 | 15 | public void Attach(IDownload download) 16 | { 17 | if (download == null) 18 | throw new ArgumentNullException("download"); 19 | 20 | lock (this.monitor) 21 | { 22 | this.attachedDownloads.Add(download); 23 | } 24 | 25 | this.OnAttach(download); 26 | } 27 | 28 | public void Detach(IDownload download) 29 | { 30 | lock (this.monitor) 31 | { 32 | this.attachedDownloads.Remove(download); 33 | } 34 | 35 | this.OnDetach(download); 36 | } 37 | 38 | public void DetachAll() 39 | { 40 | List downloadsCopy; 41 | 42 | lock (this.monitor) 43 | { 44 | downloadsCopy = new List(this.attachedDownloads); 45 | } 46 | 47 | foreach (var download in downloadsCopy) 48 | { 49 | this.Detach(download); 50 | } 51 | } 52 | 53 | public virtual void Dispose() 54 | { 55 | this.DetachAll(); 56 | } 57 | 58 | protected virtual void OnAttach(IDownload download) 59 | { 60 | } 61 | 62 | protected virtual void OnDetach(IDownload download) 63 | { 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /Downloader.Business/Observer/DownloadDataSample.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace Toqe.Downloader.Business.Observer 7 | { 8 | public class DownloadDataSample 9 | { 10 | public DateTime Timestamp { get; set; } 11 | 12 | public int Count { get; set; } 13 | } 14 | } -------------------------------------------------------------------------------- /Downloader.Business/Observer/DownloadProgressMonitor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using Toqe.Downloader.Business.Contract; 6 | using Toqe.Downloader.Business.Contract.Events; 7 | 8 | namespace Toqe.Downloader.Business.Observer 9 | { 10 | public class DownloadProgressMonitor : AbstractDownloadObserver 11 | { 12 | private readonly Dictionary downloadSizes = new Dictionary(); 13 | 14 | private readonly Dictionary alreadyDownloadedSizes = new Dictionary(); 15 | 16 | public float GetCurrentProgressPercentage(IDownload download) 17 | { 18 | lock (this.monitor) 19 | { 20 | if (!downloadSizes.ContainsKey(download) || !alreadyDownloadedSizes.ContainsKey(download) || downloadSizes[download] <= 0) 21 | { 22 | return 0; 23 | } 24 | 25 | return (float)alreadyDownloadedSizes[download] / downloadSizes[download]; 26 | } 27 | } 28 | 29 | public long GetCurrentProgressInBytes(IDownload download) 30 | { 31 | lock (this.monitor) 32 | { 33 | if (!alreadyDownloadedSizes.ContainsKey(download)) 34 | { 35 | return 0; 36 | } 37 | 38 | return alreadyDownloadedSizes[download]; 39 | } 40 | } 41 | 42 | public long GetTotalFilesizeInBytes(IDownload download) 43 | { 44 | lock (this.monitor) 45 | { 46 | if (!downloadSizes.ContainsKey(download) || downloadSizes[download] <= 0) 47 | { 48 | return 0; 49 | } 50 | 51 | return downloadSizes[download]; 52 | } 53 | } 54 | 55 | protected override void OnAttach(IDownload download) 56 | { 57 | download.DownloadStarted += OnDownloadStarted; 58 | download.DataReceived += OnDownloadDataReceived; 59 | download.DownloadCompleted += OnDownloadCompleted; 60 | } 61 | 62 | protected override void OnDetach(IDownload download) 63 | { 64 | download.DownloadStarted -= OnDownloadStarted; 65 | download.DataReceived -= OnDownloadDataReceived; 66 | download.DownloadCompleted -= OnDownloadCompleted; 67 | 68 | lock (this.monitor) 69 | { 70 | if (this.downloadSizes.ContainsKey(download)) 71 | { 72 | this.downloadSizes.Remove(download); 73 | } 74 | 75 | if (this.alreadyDownloadedSizes.ContainsKey(download)) 76 | { 77 | this.alreadyDownloadedSizes.Remove(download); 78 | } 79 | } 80 | } 81 | 82 | private void OnDownloadStarted(DownloadStartedEventArgs args) 83 | { 84 | lock (this.monitor) 85 | { 86 | this.downloadSizes[args.Download] = args.CheckResult.Size; 87 | this.alreadyDownloadedSizes[args.Download] = args.AlreadyDownloadedSize; 88 | } 89 | } 90 | 91 | private void OnDownloadDataReceived(DownloadDataReceivedEventArgs args) 92 | { 93 | lock (this.monitor) 94 | { 95 | if (!alreadyDownloadedSizes.ContainsKey(args.Download)) 96 | { 97 | this.alreadyDownloadedSizes[args.Download] = 0; 98 | } 99 | 100 | this.alreadyDownloadedSizes[args.Download] += args.Count; 101 | } 102 | } 103 | 104 | private void OnDownloadCompleted(DownloadEventArgs args) 105 | { 106 | lock (this.monitor) 107 | { 108 | this.alreadyDownloadedSizes[args.Download] = this.downloadSizes[args.Download]; 109 | } 110 | } 111 | } 112 | } -------------------------------------------------------------------------------- /Downloader.Business/Observer/DownloadSpeedMonitor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using Toqe.Downloader.Business.Contract; 6 | using Toqe.Downloader.Business.Contract.Events; 7 | 8 | namespace Toqe.Downloader.Business.Observer 9 | { 10 | public class DownloadSpeedMonitor : AbstractDownloadObserver 11 | { 12 | private readonly int maxSampleCount; 13 | 14 | private readonly List samples = new List(); 15 | 16 | public DownloadSpeedMonitor(int maxSampleCount) 17 | { 18 | if (maxSampleCount < 2) 19 | throw new ArgumentException("maxSampleCount < 2"); 20 | 21 | this.maxSampleCount = maxSampleCount; 22 | } 23 | 24 | public int GetCurrentBytesPerSecond() 25 | { 26 | lock (this.monitor) 27 | { 28 | if (this.samples.Count < 2) 29 | { 30 | return 0; 31 | } 32 | 33 | var sumOfBytesFromCalls = this.samples.Sum(s => s.Count); 34 | var ticksBetweenCalls = (DateTime.UtcNow - this.samples[0].Timestamp).Ticks; 35 | 36 | return (int)((double)sumOfBytesFromCalls / ticksBetweenCalls * 10000 * 1000); 37 | } 38 | } 39 | 40 | protected override void OnAttach(IDownload download) 41 | { 42 | download.DataReceived += downloadDataReceived; 43 | } 44 | 45 | protected override void OnDetach(IDownload download) 46 | { 47 | download.DataReceived -= downloadDataReceived; 48 | } 49 | 50 | private void AddSample(int count) 51 | { 52 | lock (this.monitor) 53 | { 54 | var sample = new DownloadDataSample() 55 | { 56 | Count = count, 57 | Timestamp = DateTime.UtcNow 58 | }; 59 | 60 | this.samples.Add(sample); 61 | 62 | if (this.samples.Count > this.maxSampleCount) 63 | { 64 | this.samples.RemoveAt(0); 65 | } 66 | } 67 | } 68 | 69 | private void downloadDataReceived(DownloadDataReceivedEventArgs args) 70 | { 71 | AddSample(args.Count); 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /Downloader.Business/Observer/DownloadThrottling.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading; 6 | using Toqe.Downloader.Business.Contract; 7 | using Toqe.Downloader.Business.Contract.Events; 8 | 9 | namespace Toqe.Downloader.Business.Observer 10 | { 11 | public class DownloadThrottling : IDownloadObserver, IDisposable 12 | { 13 | private readonly int maxBytesPerSecond; 14 | 15 | private readonly List downloads = new List(); 16 | 17 | private readonly int maxSampleCount; 18 | 19 | private List samples; 20 | 21 | private double floatingWaitingTimeInMilliseconds = 0; 22 | 23 | private object monitor = new object(); 24 | 25 | public DownloadThrottling(int maxBytesPerSecond, int maxSampleCount) 26 | { 27 | if (maxBytesPerSecond <= 0) 28 | throw new ArgumentException("maxBytesPerSecond <= 0"); 29 | 30 | if (maxSampleCount < 2) 31 | throw new ArgumentException("sampleCount < 2"); 32 | 33 | this.maxBytesPerSecond = maxBytesPerSecond; 34 | this.maxSampleCount = maxSampleCount; 35 | this.samples = new List(); 36 | } 37 | 38 | public void Attach(IDownload download) 39 | { 40 | if (download == null) 41 | throw new ArgumentNullException("download"); 42 | 43 | download.DataReceived += this.downloadDataReceived; 44 | 45 | lock (this.monitor) 46 | { 47 | this.downloads.Add(download); 48 | } 49 | } 50 | 51 | public void Detach(IDownload download) 52 | { 53 | if (download == null) 54 | throw new ArgumentNullException("download"); 55 | 56 | download.DataReceived -= this.downloadDataReceived; 57 | 58 | lock (this.monitor) 59 | { 60 | this.downloads.Remove(download); 61 | } 62 | } 63 | 64 | public void DetachAll() 65 | { 66 | lock (this.monitor) 67 | { 68 | foreach (var download in this.downloads) 69 | { 70 | this.Detach(download); 71 | } 72 | } 73 | } 74 | 75 | public void Dispose() 76 | { 77 | this.DetachAll(); 78 | } 79 | 80 | private void AddSample(int count) 81 | { 82 | lock (this.monitor) 83 | { 84 | var sample = new DownloadDataSample() 85 | { 86 | Count = count, 87 | Timestamp = DateTime.UtcNow 88 | }; 89 | 90 | this.samples.Add(sample); 91 | 92 | if (this.samples.Count > this.maxSampleCount) 93 | { 94 | this.samples.RemoveAt(0); 95 | } 96 | } 97 | } 98 | 99 | private int CalculateWaitingTime() 100 | { 101 | lock (this.monitor) 102 | { 103 | if (this.samples.Count < 2) 104 | { 105 | return 0; 106 | } 107 | 108 | var averageBytesPerCall = this.samples.Average(s => s.Count); 109 | double sumOfTicksBetweenCalls = 0; 110 | 111 | // 1 tick = 100 nano seconds, 1 ms ^= 10.000 ticks 112 | for (var i = 0; i < this.samples.Count - 1; i++) 113 | { 114 | sumOfTicksBetweenCalls += (this.samples[i + 1].Timestamp - this.samples[i].Timestamp).Ticks; 115 | } 116 | 117 | var averageTicksBetweenCalls = sumOfTicksBetweenCalls / (this.samples.Count - 1); 118 | var timePerNetworkRequestInMilliseconds = averageTicksBetweenCalls / 10000 - floatingWaitingTimeInMilliseconds; 119 | 120 | var currentBytesPerMillisecond = averageBytesPerCall / averageTicksBetweenCalls * 10000; 121 | var maxBytesPerMillisecond = (double)this.maxBytesPerSecond / 1000; 122 | 123 | var waitingTimeInMilliseconds = averageBytesPerCall / maxBytesPerMillisecond - timePerNetworkRequestInMilliseconds; 124 | this.floatingWaitingTimeInMilliseconds = waitingTimeInMilliseconds * 0.6 + this.floatingWaitingTimeInMilliseconds * 0.4; 125 | 126 | return (int)waitingTimeInMilliseconds; 127 | } 128 | } 129 | 130 | private void downloadDataReceived(DownloadDataReceivedEventArgs args) 131 | { 132 | this.AddSample(args.Count); 133 | var waitingTime = this.CalculateWaitingTime(); 134 | 135 | if (waitingTime > 0) 136 | { 137 | Thread.Sleep(waitingTime); 138 | } 139 | } 140 | } 141 | } -------------------------------------------------------------------------------- /Downloader.Business/Observer/DownloadToFileSaver.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text; 6 | 7 | using Toqe.Downloader.Business.Contract; 8 | using Toqe.Downloader.Business.Contract.Events; 9 | 10 | namespace Toqe.Downloader.Business.Observer 11 | { 12 | public class DownloadToFileSaver : AbstractDownloadObserver 13 | { 14 | private FileInfo file; 15 | 16 | private FileStream fileStream; 17 | 18 | public DownloadToFileSaver(string filename) 19 | { 20 | if (string.IsNullOrEmpty(filename)) 21 | throw new ArgumentException("filename"); 22 | 23 | this.file = new FileInfo(filename); 24 | } 25 | 26 | public DownloadToFileSaver(FileInfo file) 27 | { 28 | if (file == null) 29 | throw new ArgumentNullException("file"); 30 | 31 | this.file = file; 32 | } 33 | 34 | protected override void OnAttach(IDownload download) 35 | { 36 | download.DownloadStarted += downloadStarted; 37 | download.DownloadCancelled += downloadCancelled; 38 | download.DownloadCompleted += downloadCompleted; 39 | download.DownloadStopped += downloadStopped; 40 | download.DataReceived += downloadDataReceived; 41 | } 42 | 43 | protected override void OnDetach(IDownload download) 44 | { 45 | download.DownloadStarted -= downloadStarted; 46 | download.DownloadCancelled -= downloadCancelled; 47 | download.DownloadCompleted -= downloadCompleted; 48 | download.DownloadStopped -= downloadStopped; 49 | download.DataReceived -= downloadDataReceived; 50 | } 51 | 52 | private void OpenFileIfNecessary() 53 | { 54 | lock (this.monitor) 55 | { 56 | if (this.fileStream == null) 57 | { 58 | this.fileStream = this.file.Open(FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None); 59 | } 60 | } 61 | } 62 | 63 | private void WriteToFile(byte[] data, long offset, int count) 64 | { 65 | lock (this.monitor) 66 | { 67 | this.OpenFileIfNecessary(); 68 | 69 | this.fileStream.Position = offset; 70 | this.fileStream.Write(data, 0, count); 71 | } 72 | } 73 | 74 | private void CloseFile() 75 | { 76 | lock (this.monitor) 77 | { 78 | if (this.fileStream != null) 79 | { 80 | this.fileStream.Flush(); 81 | this.fileStream.Close(); 82 | this.fileStream.Dispose(); 83 | this.fileStream = null; 84 | } 85 | } 86 | } 87 | 88 | private void downloadDataReceived(DownloadDataReceivedEventArgs args) 89 | { 90 | lock (this.monitor) 91 | { 92 | this.WriteToFile(args.Data, args.Offset, args.Count); 93 | } 94 | } 95 | 96 | private void downloadStarted(DownloadStartedEventArgs args) 97 | { 98 | lock (this.monitor) 99 | { 100 | this.OpenFileIfNecessary(); 101 | } 102 | } 103 | 104 | private void downloadCompleted(DownloadEventArgs args) 105 | { 106 | lock (this.monitor) 107 | { 108 | this.CloseFile(); 109 | } 110 | } 111 | 112 | private void downloadStopped(DownloadEventArgs args) 113 | { 114 | lock (this.monitor) 115 | { 116 | this.CloseFile(); 117 | } 118 | } 119 | 120 | private void downloadCancelled(DownloadCancelledEventArgs args) 121 | { 122 | lock (this.monitor) 123 | { 124 | this.CloseFile(); 125 | } 126 | } 127 | 128 | public override void Dispose() 129 | { 130 | lock (this.monitor) 131 | { 132 | this.CloseFile(); 133 | } 134 | 135 | base.Dispose(); 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /Downloader.Business/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // Allgemeine Informationen über eine Assembly werden über die folgenden 6 | // Attribute gesteuert. Ändern Sie diese Attributwerte, um die Informationen zu ändern, 7 | // die mit einer Assembly verknüpft sind. 8 | [assembly: AssemblyTitle("Downloader.Business")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyCulture("")] 11 | 12 | // Die folgende GUID bestimmt die ID der Typbibliothek, wenn dieses Projekt für COM verfügbar gemacht wird 13 | [assembly: Guid("8530fe99-5a6f-4549-ae38-b70de7a5527d")] 14 | -------------------------------------------------------------------------------- /Downloader.Business/Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // Dieser Code wurde von einem Tool generiert. 4 | // Laufzeitversion:4.0.30319.34014 5 | // 6 | // Änderungen an dieser Datei können falsches Verhalten verursachen und gehen verloren, wenn 7 | // der Code erneut generiert wird. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace Toqe.Downloader.Business.Properties { 12 | using System; 13 | 14 | 15 | /// 16 | /// Eine stark typisierte Ressourcenklasse zum Suchen von lokalisierten Zeichenfolgen usw. 17 | /// 18 | // Diese Klasse wurde von der StronglyTypedResourceBuilder automatisch generiert 19 | // -Klasse über ein Tool wie ResGen oder Visual Studio automatisch generiert. 20 | // Um einen Member hinzuzufügen oder zu entfernen, bearbeiten Sie die .ResX-Datei und führen dann ResGen 21 | // mit der /str-Option erneut aus, oder Sie erstellen Ihr VS-Projekt neu. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resources() { 33 | } 34 | 35 | /// 36 | /// Gibt die zwischengespeicherte ResourceManager-Instanz zurück, die von dieser Klasse verwendet wird. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Toqe.Downloader.Business.Properties.Resources", typeof(Resources).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Überschreibt die CurrentUICulture-Eigenschaft des aktuellen Threads für alle 51 | /// Ressourcenzuordnungen, die diese stark typisierte Ressourcenklasse verwenden. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Downloader.Business/Properties/Resources.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | text/microsoft-resx 107 | 108 | 109 | 2.0 110 | 111 | 112 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 113 | 114 | 115 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | -------------------------------------------------------------------------------- /Downloader.Business/Properties/Settings.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // Dieser Code wurde von einem Tool generiert. 4 | // Laufzeitversion:4.0.30319.34014 5 | // 6 | // Änderungen an dieser Datei können falsches Verhalten verursachen und gehen verloren, wenn 7 | // der Code erneut generiert wird. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace Toqe.Downloader.Business.Properties { 12 | 13 | 14 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 15 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")] 16 | internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { 17 | 18 | private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); 19 | 20 | public static Settings Default { 21 | get { 22 | return defaultInstance; 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Downloader.Business/Properties/Settings.settings: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Downloader.Business/Utils/DownloadChecker.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net; 4 | using System.Text; 5 | using Toqe.Downloader.Business.Contract; 6 | 7 | namespace Toqe.Downloader.Business.Utils 8 | { 9 | public class DownloadChecker : IDownloadChecker 10 | { 11 | public DownloadCheckResult CheckDownload(WebResponse response) 12 | { 13 | var result = new DownloadCheckResult(); 14 | var acceptRanges = response.Headers["Accept-Ranges"]; 15 | result.SupportsResume = !string.IsNullOrEmpty(acceptRanges) && acceptRanges.ToLower().Contains("bytes"); 16 | result.Size = response.ContentLength; 17 | result.StatusCode = (int?)(response as HttpWebResponse)?.StatusCode; 18 | result.Success = true; 19 | return result; 20 | } 21 | 22 | public DownloadCheckResult CheckDownload(Uri url, IWebRequestBuilder requestBuilder) 23 | { 24 | try 25 | { 26 | var request = requestBuilder.CreateRequest(url, null); 27 | 28 | using (var response = request.GetResponse()) 29 | { 30 | return CheckDownload(response); 31 | } 32 | } 33 | catch (WebException ex) 34 | { 35 | return new DownloadCheckResult() { Exception = ex, StatusCode = (int)(ex.Response as HttpWebResponse)?.StatusCode }; 36 | } 37 | catch (Exception ex) 38 | { 39 | return new DownloadCheckResult() { Exception = ex }; 40 | } 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /Downloader.Business/Utils/DownloadRangeHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using Toqe.Downloader.Business.Contract; 5 | 6 | namespace Toqe.Downloader.Business.Utils 7 | { 8 | public class DownloadRangeHelper 9 | { 10 | public bool RangesCollide(DownloadRange range1, DownloadRange range2) 11 | { 12 | return range1.Start <= range2.End && range2.Start <= range1.End; 13 | } 14 | 15 | public List RangeDifference(DownloadRange fullRange, DownloadRange range) 16 | { 17 | var result = new List(); 18 | 19 | // no intersection 20 | if (!RangesCollide(fullRange, range)) 21 | { 22 | result.Add(fullRange); 23 | return result; 24 | } 25 | 26 | // fullRange is part of range --> difference is empty 27 | if (fullRange.Start >= range.Start && fullRange.End <= range.End) 28 | { 29 | return result; 30 | } 31 | 32 | if (fullRange.Start < range.Start) 33 | { 34 | result.Add(new DownloadRange(fullRange.Start, range.Start - fullRange.Start)); 35 | } 36 | 37 | if (fullRange.End > range.End) 38 | { 39 | result.Add(new DownloadRange(range.End + 1, fullRange.End - range.End)); 40 | } 41 | 42 | return result; 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /Downloader.Business/Utils/SimpleWebRequestBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Reflection; 6 | using System.Text; 7 | using Toqe.Downloader.Business.Contract; 8 | 9 | namespace Toqe.Downloader.Business.Utils 10 | { 11 | public class SimpleWebRequestBuilder : IWebRequestBuilder 12 | { 13 | public SimpleWebRequestBuilder(IWebProxy proxy) 14 | { 15 | this.proxy = proxy; 16 | } 17 | 18 | public SimpleWebRequestBuilder() 19 | : this(null) 20 | { 21 | } 22 | 23 | public IWebProxy proxy { get; private set; } 24 | 25 | public HttpWebRequest CreateRequest(Uri url, long? offset) 26 | { 27 | var request = (HttpWebRequest)WebRequest.Create(url); 28 | 29 | if (proxy != null) 30 | { 31 | request.Proxy = proxy; 32 | } 33 | 34 | if (offset.HasValue && offset.Value > 0) 35 | { 36 | // Only works with long values starting with .NET 4: 37 | // request.AddRange(offset.Value); 38 | 39 | this.AddLongRangeInDotNet3_5(request, offset.Value); 40 | } 41 | 42 | return request; 43 | } 44 | 45 | private void AddLongRangeInDotNet3_5(HttpWebRequest request, long offset) 46 | { 47 | var method = typeof(WebHeaderCollection).GetMethod("AddWithoutValidate", BindingFlags.Instance | BindingFlags.NonPublic); 48 | 49 | string key = "Range"; 50 | string val = string.Format("bytes={0}-", offset); 51 | 52 | method.Invoke(request.Headers, new object[] { key, val }); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Downloader.Example/Downloader.Example.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {2BB3FC3C-173E-4F3A-8F63-0896606F106F} 8 | Exe 9 | Downloader.Example 10 | Downloader.Example 11 | v3.5 12 | 512 13 | 14 | 15 | AnyCPU 16 | true 17 | full 18 | false 19 | ..\bin\ 20 | DEBUG;TRACE 21 | prompt 22 | 4 23 | 24 | 25 | AnyCPU 26 | pdbonly 27 | true 28 | ..\bin\ 29 | TRACE 30 | prompt 31 | 4 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | Properties\GlobalAssemblyInfo.cs 44 | 45 | 46 | 47 | 48 | 49 | 50 | {edebc948-ab61-479a-9ac5-99c2517ddb9d} 51 | Downloader.Business.Contract 52 | 53 | 54 | {045ce992-f761-4541-be07-d9da5b1e37c5} 55 | Downloader.Business 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /Downloader.Example/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | using Toqe.Downloader.Business.Contract; 5 | using Toqe.Downloader.Business.Contract.Events; 6 | using Toqe.Downloader.Business.Download; 7 | using Toqe.Downloader.Business.DownloadBuilder; 8 | using Toqe.Downloader.Business.Observer; 9 | using Toqe.Downloader.Business.Utils; 10 | 11 | namespace Downloader.Example 12 | { 13 | public class Program 14 | { 15 | static bool finished = false; 16 | 17 | public static void Main() 18 | { 19 | bool useDownloadSpeedThrottling = false; 20 | 21 | // Please insert an URL of a large file here, otherwise the download will be finished too quickly to really demonstrate the functionality. 22 | var url = new Uri("https://raw.githubusercontent.com/Toqe/Downloader/master/README.md"); 23 | var file = new System.IO.FileInfo("README.md"); 24 | var requestBuilder = new SimpleWebRequestBuilder(); 25 | var dlChecker = new DownloadChecker(); 26 | var httpDlBuilder = new SimpleDownloadBuilder(requestBuilder, dlChecker); 27 | var timeForHeartbeat = 3000; 28 | var timeToRetry = 5000; 29 | var maxRetries = 5; 30 | var resumingDlBuilder = new ResumingDownloadBuilder(timeForHeartbeat, timeToRetry, maxRetries, httpDlBuilder); 31 | List alreadyDownloadedRanges = null; 32 | var bufferSize = 4096; 33 | var numberOfParts = 4; 34 | var download = new MultiPartDownload(url, bufferSize, numberOfParts, resumingDlBuilder, requestBuilder, dlChecker, alreadyDownloadedRanges); 35 | var speedMonitor = new DownloadSpeedMonitor(maxSampleCount: 128); 36 | speedMonitor.Attach(download); 37 | var progressMonitor = new DownloadProgressMonitor(); 38 | progressMonitor.Attach(download); 39 | 40 | if (useDownloadSpeedThrottling) 41 | { 42 | var downloadThrottling = new DownloadThrottling(maxBytesPerSecond: 200 * 1024, maxSampleCount: 128); 43 | downloadThrottling.Attach(download); 44 | } 45 | 46 | var dlSaver = new DownloadToFileSaver(file); 47 | dlSaver.Attach(download); 48 | download.DownloadCompleted += OnCompleted; 49 | download.Start(); 50 | 51 | while (!finished) 52 | { 53 | Thread.Sleep(1000); 54 | 55 | var alreadyDownloadedSizeInBytes = progressMonitor.GetCurrentProgressInBytes(download); 56 | var totalDownloadSizeInBytes = progressMonitor.GetTotalFilesizeInBytes(download); 57 | var currentSpeedInBytesPerSecond = speedMonitor.GetCurrentBytesPerSecond(); 58 | 59 | var currentProgressInPercent = progressMonitor.GetCurrentProgressPercentage(download) * 100; 60 | var alreadyDownloadedSizeInKiB = (alreadyDownloadedSizeInBytes / 1024); 61 | var totalDownloadSizeInKiB = (totalDownloadSizeInBytes / 1024); 62 | var currentSpeedInKiBPerSecond = (currentSpeedInBytesPerSecond / 1024); 63 | var remainingTimeInSeconds = currentSpeedInBytesPerSecond == 0 ? 0 : (totalDownloadSizeInBytes - alreadyDownloadedSizeInBytes) / currentSpeedInBytesPerSecond; 64 | 65 | Console.WriteLine( 66 | "Progress: " + currentProgressInPercent + "% " + "(" + alreadyDownloadedSizeInKiB + " of " + totalDownloadSizeInKiB + " KiB)" + 67 | " Speed: " + currentSpeedInKiBPerSecond + " KiB/sec." + 68 | " Remaining time: " + remainingTimeInSeconds + " sec."); 69 | } 70 | } 71 | 72 | static void OnCompleted(DownloadEventArgs args) 73 | { 74 | // this is an important thing to do after a download isn't used anymore, otherwise you will run into a memory leak. 75 | args.Download.DetachAllHandlers(); 76 | Console.WriteLine("Download has finished!"); 77 | finished = true; 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /Downloader.Example/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // Allgemeine Informationen über eine Assembly werden über die folgenden 6 | // Attribute gesteuert. Ändern Sie diese Attributwerte, um die Informationen zu ändern, 7 | // die einer Assembly zugeordnet sind. 8 | [assembly: AssemblyTitle("Downloader.Example")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyCulture("")] 11 | 12 | // Die folgende GUID bestimmt die ID der Typbibliothek, wenn dieses Projekt für COM verfügbar gemacht wird 13 | [assembly: Guid("2bb3fc3c-173e-4f3a-8f63-0896606f106f")] 14 | -------------------------------------------------------------------------------- /Downloader.Test/DownloadRangeHelperTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using Toqe.Downloader.Business.Contract; 6 | using Toqe.Downloader.Business.Utils; 7 | using Xunit; 8 | 9 | namespace Downloader.Test 10 | { 11 | public class DownloadRangeHelperTest 12 | { 13 | private DownloadRangeHelper helper = new DownloadRangeHelper(); 14 | 15 | [Fact] 16 | public void NoIntersection() 17 | { 18 | var fullRange = new DownloadRange(1, 8); 19 | var range1 = new DownloadRange(9, 3); 20 | var range2 = new DownloadRange(0, 1); 21 | 22 | Assert.False(helper.RangesCollide(fullRange, range1)); 23 | Assert.False(helper.RangesCollide(fullRange, range2)); 24 | 25 | var differenceWithRange1 = helper.RangeDifference(fullRange, range1); 26 | var differenceWithRange2 = helper.RangeDifference(fullRange, range2); 27 | 28 | Assert.Equal(1, differenceWithRange1.Count); 29 | Assert.Equal(1, differenceWithRange2.Count); 30 | Assert.Contains(fullRange, differenceWithRange1); 31 | Assert.Contains(fullRange, differenceWithRange2); 32 | } 33 | 34 | [Fact] 35 | public void FullOverlay() 36 | { 37 | var fullRange = new DownloadRange(1, 8); 38 | var range1 = new DownloadRange(0, 10); 39 | var range2 = new DownloadRange(1, 8); 40 | 41 | Assert.True(helper.RangesCollide(fullRange, range1)); 42 | Assert.True(helper.RangesCollide(fullRange, range2)); 43 | 44 | var differenceWithRange1 = helper.RangeDifference(fullRange, range1); 45 | var differenceWithRange2 = helper.RangeDifference(fullRange, range2); 46 | 47 | Assert.Empty(differenceWithRange1); 48 | Assert.Empty(differenceWithRange2); 49 | } 50 | 51 | [Fact] 52 | public void PartialIntersectionWithOneResult() 53 | { 54 | var fullRange = new DownloadRange(1, 8); 55 | var range1 = new DownloadRange(0, 5); 56 | var range2 = new DownloadRange(1, 4); 57 | var range3 = new DownloadRange(7, 4); 58 | var range4 = new DownloadRange(8, 4); 59 | 60 | Assert.True(helper.RangesCollide(fullRange, range1)); 61 | Assert.True(helper.RangesCollide(fullRange, range2)); 62 | Assert.True(helper.RangesCollide(fullRange, range3)); 63 | Assert.True(helper.RangesCollide(fullRange, range4)); 64 | 65 | var differenceWithRange1 = helper.RangeDifference(fullRange, range1); 66 | var differenceWithRange2 = helper.RangeDifference(fullRange, range2); 67 | var differenceWithRange3 = helper.RangeDifference(fullRange, range3); 68 | var differenceWithRange4 = helper.RangeDifference(fullRange, range4); 69 | 70 | Assert.Equal(1, differenceWithRange1.Count); 71 | Assert.Equal(1, differenceWithRange2.Count); 72 | Assert.Equal(1, differenceWithRange3.Count); 73 | Assert.Equal(1, differenceWithRange4.Count); 74 | Assert.Contains(new DownloadRange(5, 4), differenceWithRange1); 75 | Assert.Contains(new DownloadRange(5, 4), differenceWithRange2); 76 | Assert.Contains(new DownloadRange(1, 6), differenceWithRange3); 77 | Assert.Contains(new DownloadRange(1, 7), differenceWithRange4); 78 | } 79 | 80 | [Fact] 81 | public void PartialIntersectionWithTwoResults() 82 | { 83 | var fullRange = new DownloadRange(1, 8); 84 | var range1 = new DownloadRange(2, 1); 85 | var range2 = new DownloadRange(3, 3); 86 | 87 | Assert.True(helper.RangesCollide(fullRange, range1)); 88 | Assert.True(helper.RangesCollide(fullRange, range2)); 89 | 90 | var differenceWithRange1 = helper.RangeDifference(fullRange, range1); 91 | var differenceWithRange2 = helper.RangeDifference(fullRange, range2); 92 | 93 | Assert.Equal(2, differenceWithRange1.Count); 94 | Assert.Equal(2, differenceWithRange2.Count); 95 | Assert.Contains(new DownloadRange(1, 1), differenceWithRange1); 96 | Assert.Contains(new DownloadRange(3, 6), differenceWithRange1); 97 | Assert.Contains(new DownloadRange(1, 2), differenceWithRange2); 98 | Assert.Contains(new DownloadRange(6, 3), differenceWithRange2); 99 | } 100 | } 101 | } -------------------------------------------------------------------------------- /Downloader.Test/Downloader.Test.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {B4B0E523-1273-46AD-A956-7B66390F6FA9} 8 | Library 9 | Properties 10 | Toqe.Downloader.Test 11 | Toqe.Downloader.Test 12 | v3.5 13 | 512 14 | ..\ 15 | true 16 | 17 | 18 | 19 | true 20 | full 21 | false 22 | ..\bin\ 23 | DEBUG;TRACE 24 | prompt 25 | 4 26 | 27 | 28 | pdbonly 29 | true 30 | ..\bin\ 31 | TRACE 32 | prompt 33 | 4 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | ..\packages\xunit.1.9.2\lib\net20\xunit.dll 44 | 45 | 46 | 47 | 48 | Properties\GlobalAssemblyInfo.cs 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | {edebc948-ab61-479a-9ac5-99c2517ddb9d} 64 | Downloader.Business.Contract 65 | 66 | 67 | {045ce992-f761-4541-be07-d9da5b1e37c5} 68 | Downloader.Business 69 | 70 | 71 | 72 | 79 | -------------------------------------------------------------------------------- /Downloader.Test/MultiPartDownloadTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using Toqe.Downloader.Business.Contract.Events; 6 | using Toqe.Downloader.Business.DownloadBuilder; 7 | using Xunit; 8 | 9 | namespace Downloader.Test 10 | { 11 | public class MultiPartDownloadTest 12 | { 13 | private static readonly Uri url = new Uri("http://test.com"); 14 | 15 | private static readonly int bufferSize = 4096; 16 | 17 | private static readonly int numberOfParts = 4; 18 | 19 | [Fact] 20 | public void TestMultiPartDownloadListsDuringDownload() 21 | { 22 | var dlBuilder = new TestDownloadBuilder(); 23 | var requestBuilder = new TestWebRequestBuilder(); 24 | var dlChecker = new TestDownloadChecker(); 25 | var mpdlBuilder = new MultiPartDownloadBuilder(numberOfParts, dlBuilder, requestBuilder, dlChecker, null); 26 | var dl = mpdlBuilder.Build(url, bufferSize, null, null); 27 | 28 | var dataReceivedList = new List(); 29 | var downloadStartedList = new List(); 30 | var downloadCompletedList = new List(); 31 | var downloadStoppedList = new List(); 32 | var downloadCancelledList = new List(); 33 | 34 | // TODO: Register events and add args to list, if handler is called 35 | 36 | dl.Start(); 37 | 38 | // TODO: wait for download to build up 39 | 40 | // TODO: simulate download parts and check for correct results 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Downloader.Test/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // Allgemeine Informationen über eine Assembly werden über die folgenden 6 | // Attribute gesteuert. Ändern Sie diese Attributwerte, um die Informationen zu ändern, 7 | // die mit einer Assembly verknüpft sind. 8 | [assembly: AssemblyTitle("Downloader.Test")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyCulture("")] 11 | 12 | // Die folgende GUID bestimmt die ID der Typbibliothek, wenn dieses Projekt für COM verfügbar gemacht wird 13 | [assembly: Guid("5430c721-ad4c-49a7-aa23-1314dfaabc51")] 14 | -------------------------------------------------------------------------------- /Downloader.Test/TestDownload.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using Toqe.Downloader.Business.Contract; 6 | using Toqe.Downloader.Business.Contract.Events; 7 | using Toqe.Downloader.Business.Download; 8 | 9 | namespace Downloader.Test 10 | { 11 | public class TestDownload : AbstractDownload 12 | { 13 | public TestDownload(Uri url, int bufferSize) 14 | : base(url, bufferSize, null, null, null, null) 15 | { 16 | } 17 | 18 | public void OnDataReceived(byte[] data, long offset, int count) 19 | { 20 | this.OnDataReceived(new DownloadDataReceivedEventArgs(this, data, offset, count)); 21 | } 22 | 23 | public void OnDownloadStarted(DownloadCheckResult checkResult) 24 | { 25 | this.OnDownloadStarted(new DownloadStartedEventArgs(this, checkResult)); 26 | } 27 | 28 | public void OnDownloadCompleted() 29 | { 30 | this.OnDownloadCompleted(new DownloadEventArgs(this)); 31 | } 32 | 33 | public void OnDownloadStopped() 34 | { 35 | this.OnDownloadStopped(new DownloadEventArgs(this)); 36 | } 37 | 38 | public void OnDownloadCancelled(Exception ex) 39 | { 40 | this.OnDownloadCancelled(new DownloadCancelledEventArgs(this, ex)); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Downloader.Test/TestDownloadBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using Toqe.Downloader.Business.Contract; 6 | 7 | namespace Downloader.Test 8 | { 9 | public class TestDownloadBuilder : IDownloadBuilder 10 | { 11 | public TestDownloadBuilder() 12 | { 13 | this.ReturnedDownloads = new List(); 14 | } 15 | 16 | public List ReturnedDownloads { get; set; } 17 | 18 | public IDownload Build(Uri url, int bufferSize, long? offset, long? maxReadBytes) 19 | { 20 | var download = new TestDownload(url, bufferSize); 21 | this.ReturnedDownloads.Add(download); 22 | return download; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Downloader.Test/TestDownloadChecker.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using Toqe.Downloader.Business.Contract; 6 | 7 | namespace Downloader.Test 8 | { 9 | public class TestDownloadChecker : IDownloadChecker 10 | { 11 | public DownloadCheckResult CheckDownload(Uri url, IWebRequestBuilder requestBuilder) 12 | { 13 | return this.CheckDownload(null); 14 | } 15 | 16 | public DownloadCheckResult CheckDownload(System.Net.WebResponse response) 17 | { 18 | return new DownloadCheckResult() { Size = 4000, SupportsResume = true }; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Downloader.Test/TestWebRequestBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using Toqe.Downloader.Business.Contract; 6 | 7 | namespace Downloader.Test 8 | { 9 | public class TestWebRequestBuilder : IWebRequestBuilder 10 | { 11 | public System.Net.HttpWebRequest CreateRequest(Uri url, long? offset) 12 | { 13 | throw new NotImplementedException(); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Downloader.Test/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | -------------------------------------------------------------------------------- /Downloader.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26228.4 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Downloader.Business", "Downloader.Business\Downloader.Business.csproj", "{045CE992-F761-4541-BE07-D9DA5B1E37C5}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Downloader.Test", "Downloader.Test\Downloader.Test.csproj", "{B4B0E523-1273-46AD-A956-7B66390F6FA9}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{B65C6B7C-FB57-428F-BCEA-2E6113FA4B90}" 11 | ProjectSection(SolutionItems) = preProject 12 | .nuget\NuGet.Config = .nuget\NuGet.Config 13 | EndProjectSection 14 | EndProject 15 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Downloader.Business.Contract", "Downloader.Business.Contract\Downloader.Business.Contract.csproj", "{EDEBC948-AB61-479A-9AC5-99C2517DDB9D}" 16 | EndProject 17 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Downloader.Example", "Downloader.Example\Downloader.Example.csproj", "{2BB3FC3C-173E-4F3A-8F63-0896606F106F}" 18 | EndProject 19 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Other items", "Other items", "{84246896-DFEF-4A11-9EF5-5BBA3EFD1D45}" 20 | ProjectSection(SolutionItems) = preProject 21 | .gitattributes = .gitattributes 22 | .gitignore = .gitignore 23 | GlobalAssemblyInfo.cs = GlobalAssemblyInfo.cs 24 | LICENSE = LICENSE 25 | README.md = README.md 26 | EndProjectSection 27 | EndProject 28 | Global 29 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 30 | Debug|Any CPU = Debug|Any CPU 31 | Debug3.5|Any CPU = Debug3.5|Any CPU 32 | Release|Any CPU = Release|Any CPU 33 | Release3.5|Any CPU = Release3.5|Any CPU 34 | EndGlobalSection 35 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 36 | {045CE992-F761-4541-BE07-D9DA5B1E37C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {045CE992-F761-4541-BE07-D9DA5B1E37C5}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {045CE992-F761-4541-BE07-D9DA5B1E37C5}.Debug3.5|Any CPU.ActiveCfg = Debug|Any CPU 39 | {045CE992-F761-4541-BE07-D9DA5B1E37C5}.Debug3.5|Any CPU.Build.0 = Debug|Any CPU 40 | {045CE992-F761-4541-BE07-D9DA5B1E37C5}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {045CE992-F761-4541-BE07-D9DA5B1E37C5}.Release|Any CPU.Build.0 = Release|Any CPU 42 | {045CE992-F761-4541-BE07-D9DA5B1E37C5}.Release3.5|Any CPU.ActiveCfg = Release|Any CPU 43 | {045CE992-F761-4541-BE07-D9DA5B1E37C5}.Release3.5|Any CPU.Build.0 = Release|Any CPU 44 | {B4B0E523-1273-46AD-A956-7B66390F6FA9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 45 | {B4B0E523-1273-46AD-A956-7B66390F6FA9}.Debug|Any CPU.Build.0 = Debug|Any CPU 46 | {B4B0E523-1273-46AD-A956-7B66390F6FA9}.Debug3.5|Any CPU.ActiveCfg = Debug|Any CPU 47 | {B4B0E523-1273-46AD-A956-7B66390F6FA9}.Debug3.5|Any CPU.Build.0 = Debug|Any CPU 48 | {B4B0E523-1273-46AD-A956-7B66390F6FA9}.Release|Any CPU.ActiveCfg = Release|Any CPU 49 | {B4B0E523-1273-46AD-A956-7B66390F6FA9}.Release|Any CPU.Build.0 = Release|Any CPU 50 | {B4B0E523-1273-46AD-A956-7B66390F6FA9}.Release3.5|Any CPU.ActiveCfg = Release|Any CPU 51 | {B4B0E523-1273-46AD-A956-7B66390F6FA9}.Release3.5|Any CPU.Build.0 = Release|Any CPU 52 | {EDEBC948-AB61-479A-9AC5-99C2517DDB9D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 53 | {EDEBC948-AB61-479A-9AC5-99C2517DDB9D}.Debug|Any CPU.Build.0 = Debug|Any CPU 54 | {EDEBC948-AB61-479A-9AC5-99C2517DDB9D}.Debug3.5|Any CPU.ActiveCfg = Debug|Any CPU 55 | {EDEBC948-AB61-479A-9AC5-99C2517DDB9D}.Debug3.5|Any CPU.Build.0 = Debug|Any CPU 56 | {EDEBC948-AB61-479A-9AC5-99C2517DDB9D}.Release|Any CPU.ActiveCfg = Release|Any CPU 57 | {EDEBC948-AB61-479A-9AC5-99C2517DDB9D}.Release|Any CPU.Build.0 = Release|Any CPU 58 | {EDEBC948-AB61-479A-9AC5-99C2517DDB9D}.Release3.5|Any CPU.ActiveCfg = Release|Any CPU 59 | {EDEBC948-AB61-479A-9AC5-99C2517DDB9D}.Release3.5|Any CPU.Build.0 = Release|Any CPU 60 | {2BB3FC3C-173E-4F3A-8F63-0896606F106F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 61 | {2BB3FC3C-173E-4F3A-8F63-0896606F106F}.Debug|Any CPU.Build.0 = Debug|Any CPU 62 | {2BB3FC3C-173E-4F3A-8F63-0896606F106F}.Debug3.5|Any CPU.ActiveCfg = Debug|Any CPU 63 | {2BB3FC3C-173E-4F3A-8F63-0896606F106F}.Debug3.5|Any CPU.Build.0 = Debug|Any CPU 64 | {2BB3FC3C-173E-4F3A-8F63-0896606F106F}.Release|Any CPU.ActiveCfg = Release|Any CPU 65 | {2BB3FC3C-173E-4F3A-8F63-0896606F106F}.Release|Any CPU.Build.0 = Release|Any CPU 66 | {2BB3FC3C-173E-4F3A-8F63-0896606F106F}.Release3.5|Any CPU.ActiveCfg = Release|Any CPU 67 | {2BB3FC3C-173E-4F3A-8F63-0896606F106F}.Release3.5|Any CPU.Build.0 = Release|Any CPU 68 | EndGlobalSection 69 | GlobalSection(SolutionProperties) = preSolution 70 | HideSolutionNode = FALSE 71 | EndGlobalSection 72 | GlobalSection(ExtensibilityGlobals) = postSolution 73 | SolutionGuid = {B5E8FB73-DDDD-4712-8B83-198B04706DDD} 74 | EndGlobalSection 75 | EndGlobal 76 | -------------------------------------------------------------------------------- /GlobalAssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using System.Runtime.InteropServices; 4 | 5 | [assembly: ComVisible(false)] 6 | [assembly: CLSCompliant(true)] 7 | 8 | [assembly: AssemblyProduct("Downloader")] 9 | [assembly: AssemblyCompany("created by Tobias Ebert")] 10 | [assembly: AssemblyCopyright("MIT license")] 11 | 12 | [assembly: AssemblyVersion("1.0.0.0")] 13 | [assembly: AssemblyFileVersion("1.0.0.0")] 14 | 15 | #if DEBUG 16 | [assembly : AssemblyConfiguration("Debug")] 17 | #else 18 | [assembly: AssemblyConfiguration("Release")] 19 | #endif -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Tobias Ebert 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Downloader 2 | ========== 3 | 4 | # Update on 27.07.2024 5 | Please consider this library archived as it has been written 10 years ago with targetting .NET 3.5 in mind, using from today's perspective ancient tools (explicit Threads instead of Tasks/Channels, much explicit locking instead of concurrent data structures, etc.). I also haven't used it myself in a long time, so I can't keep supporting it. 6 | 7 | If you are looking for a more recent fork, please check out https://github.com/Myitian/Downloader which also provides a NuGet package for easy use. 8 | 9 | If you are looking for a more modern library using .NET Standard / .NET (Core) 6+, have a look at https://github.com/bezzad/Downloader 10 | 11 | And to you, DELL SupportAssist team: Thank you for crediting me at https://www.dell.com/support/manuals/de-de/support-assist-os-recovery/saosr_licensing_doc/toqe.downloader.business-license - but next time I would suggest using separate namespaces for your own code, so that the error stacktraces floating around the web won't be credited to this library ;) 12 | 13 | # General Information 14 | 15 | A library for resuming and multi-part/multi-threaded downloads in .NET written in C# 16 | 17 | The library uses .NET 3.5 and threads to support all platforms from Windows Vista on. 18 | 19 | Example for usage: 20 | Start a resuming download with 4 parts of this README.md to local file README.md. 21 | 22 | ```C# 23 | var url = new Uri("https://raw.githubusercontent.com/Toqe/Downloader/master/README.md"); 24 | var file = new System.IO.FileInfo("README.md"); 25 | var requestBuilder = new SimpleWebRequestBuilder(); 26 | var dlChecker = new DownloadChecker(); 27 | var httpDlBuilder = new SimpleDownloadBuilder(requestBuilder, dlChecker); 28 | var timeForHeartbeat = 3000; 29 | var timeToRetry = 5000; 30 | var maxRetries = 5; 31 | var rdlBuilder = new ResumingDownloadBuilder(timeForHeartbeat, timeToRetry, maxRetries, httpDlBuilder); 32 | List alreadyDownloadedRanges = null; 33 | var bufferSize = 4096; 34 | var numberOfParts = 4; 35 | var download = new MultiPartDownload(url, bufferSize, numberOfParts, rdlBuilder, requestBuilder, dlChecker, alreadyDownloadedRanges); 36 | download.DownloadCompleted += (args) => Console.WriteLine("download has finished!"); 37 | var dlSaver = new DownloadToFileSaver(file); 38 | dlSaver.Attach(download); 39 | download.Start(); 40 | ``` 41 | 42 | For a more sophisticated example also demonstrating the download observers functionality, please have a look at the Downloader.Example project. 43 | 44 | ## Note on the number of concurrent/parallel downloads ## 45 | .NET by default limits the number of concurrent connections. You can bypass this limit by manually setting the static `System.Net.ServicePointManager.DefaultConnectionLimit` property to a value appropriate to your application. Please also have a look at the documentation in the [MSDN](https://msdn.microsoft.com/en-us/library/system.net.servicepointmanager.defaultconnectionlimit.aspx) 46 | --------------------------------------------------------------------------------