/// <summary> /// Main entry point /// </summary> /// <param name="args">Arguments to be passed to the application</param> public static void Main(string[] args) { try { Downloader.ProgressUpdateEvent += Print; string retry; do { Print("How many parallel downloads do you want to execute?"); int numOfParallelDownloads = int.Parse(Console.ReadLine()); DownloadResult result = Downloader.Download(ConfigurationManager.AppSettings["fileUrl"], ConfigurationManager.AppSettings["downloadLocation"], numOfParallelDownloads); Print($"Download Summary:\n FileSize: {DisplayFormatHelper.FormatSize(result.Size)}\n Number of chunks: {numOfParallelDownloads}" + $"\n Chunk size: {DisplayFormatHelper.FormatSize(result.ChunkSize)}\n Time taken : {DisplayFormatHelper.TimeSpanDisplayFormat(result.TimeTaken)} \n Downloaded File: {result.FilePath}"); Print("Try again? (Y/N)"); retry = Console.ReadLine(); }while (!string.IsNullOrWhiteSpace(retry) && retry.ToLower() == "y"); } catch (FriendlyException ex) { Console.WriteLine(ex.Message); } Console.Read(); }
private static void UpdateProgress(long chunkSize, TimeSpan chunkTime, TimeSpan totalTime, long total, int parallelDownloads) { if (ProgressUpdateEvent == null) { return; } StringBuilder sb = new StringBuilder(); //Given the size of the chunk just downloaded and the chunks remaining, calculate the time remaining for the download to complete. sb.Append($"{downloadedChunks * 100 / total}% - Speed {DisplayFormatHelper.FormatSize((long)(chunkSize / chunkTime.TotalSeconds))}ps"); if (total > downloadedChunks) { //Assuming all the downloads start at relatively the same time ((total - downloadedChunks) * chunkTime.Ticks) - chunkTime.Ticks)) sb.Append($" - Estimated time remaining {DisplayFormatHelper.TimeSpanDisplayFormat((long)((total - downloadedChunks) * chunkTime.Ticks) - chunkTime.Ticks)}"); } ProgressUpdateEvent(sb.ToString()); }
public static DownloadResult Download(string fileUrl, string destinationFolderPath, int numberOfParallelDownloads) { downloadedChunks = 0; Uri uri = new Uri(fileUrl); //Input validation if (!Directory.Exists(destinationFolderPath)) { throw new FriendlyException($"Invalid value for destinationFolderPath. Directory {destinationFolderPath} does not exist."); } if (numberOfParallelDownloads <= 0) { throw new FriendlyException("Invalid value for numberOfParallelDownloads. Please enter a value greater than zero."); } //Calculate destination path String destinationFilePath = Path.Combine(destinationFolderPath, uri.Segments.Last()); DownloadResult result = new DownloadResult() { FilePath = destinationFilePath }; #region Get file size WebRequest webRequest = HttpWebRequest.Create(fileUrl); webRequest.Method = "HEAD"; long responseLength; using (WebResponse webResponse = webRequest.GetResponse()) { if (!webResponse.Headers.AllKeys.Contains("Content-Length")) { throw new FriendlyException("Unable to download file. Content-Length not present."); } responseLength = long.Parse(webResponse.Headers.Get("Content-Length")); result.Size = responseLength; } #endregion UpdateProgress($"File Size:{responseLength} bytes"); if (File.Exists(destinationFilePath)) { File.Delete(destinationFilePath); } if (responseLength < numberOfParallelDownloads) { throw new FriendlyException($"The file is too small to be divided into chunks to have {numberOfParallelDownloads} parallel downloads. Please select a value less than {numberOfParallelDownloads}."); } UpdateProgress("Dividing in to chunks..."); ConcurrentDictionary <long, string> tempFilesDictionary = new ConcurrentDictionary <long, string>(); #region Calculate ranges List <DataRange> readRanges = new List <DataRange>(); for (int chunk = 0; chunk < numberOfParallelDownloads - 1; chunk++) { DataRange range = new DataRange() { Start = chunk * (responseLength / numberOfParallelDownloads), End = ((chunk + 1) * (responseLength / numberOfParallelDownloads)) - 1 }; readRanges.Add(range); } readRanges.Add(new DataRange() { Start = readRanges.Any() ? readRanges.Last().End + 1 : 0, End = responseLength - 1 }); result.ChunkSize = readRanges[0].End - readRanges[0].Start; #endregion UpdateProgress($"Divided into {numberOfParallelDownloads} chunks of {readRanges[0].End - readRanges[0].Start} bytes each."); DateTime startTime = DateTime.Now; #region Parallel download long total = readRanges.Count(); UpdateProgress("Starting downloads..."); Parallel.ForEach(readRanges, new ParallelOptions() { MaxDegreeOfParallelism = numberOfParallelDownloads }, readRange => { DateTime chunkStart = DateTime.Now; HttpWebRequest httpWebRequest = HttpWebRequest.Create(fileUrl) as HttpWebRequest; httpWebRequest.Method = "GET"; httpWebRequest.AddRange(readRange.Start, readRange.End); using (HttpWebResponse httpWebResponse = httpWebRequest.GetResponse() as HttpWebResponse) { String tempFilePath = Path.GetTempFileName(); using (FileStream fileStream = new FileStream(tempFilePath, FileMode.Create, FileAccess.Write, FileShare.Write)) { httpWebResponse.GetResponseStream().CopyTo(fileStream); tempFilesDictionary.TryAdd(readRange.Start, tempFilePath); } } downloadedChunks++; UpdateProgress(readRange.End - readRange.Start, DateTime.Now - chunkStart, DateTime.Now - startTime, total, numberOfParallelDownloads); }); #endregion result.TimeTaken = DateTime.Now.Subtract(startTime); UpdateProgress($"Total time for downloading : {DisplayFormatHelper.TimeSpanDisplayFormat(result.TimeTaken)}"); UpdateProgress("Merging chunks.."); #region Merge to single file using (FileStream destinationStream = new FileStream(destinationFilePath, FileMode.Append)) { foreach (KeyValuePair <long, string> tempFile in tempFilesDictionary.OrderBy(b => b.Key)) { byte[] tempFileBytes = File.ReadAllBytes(tempFile.Value); destinationStream.Write(tempFileBytes, 0, tempFileBytes.Length); File.Delete(tempFile.Value); } #endregion } result.TimeTaken = DateTime.Now.Subtract(startTime); UpdateProgress("Process complete!"); return(result); }