/// <summary> /// Download content /// </summary> /// <param name="files">The files to download</param> public void Add(IEnumerable <UpdateFile> files) { var contentDownloader = new ContentDownloader(); contentDownloader.OnDownloadProgress += ContentDownloader_OnDownloadProgress; var hashChecker = new ContentHash(); hashChecker.OnHashingProgress += HashChecker_OnHashingProgress; var cancellationSource = new CancellationTokenSource(); var progressData = new ContentOperationProgress(); foreach (var file in files) { progressData.CurrentOperation = OperationType.DownloadFileStart; progressData.File = file; Progress?.Invoke(this, progressData); if (Contains(file)) { progressData.CurrentOperation = OperationType.DownloadFileEnd; Progress?.Invoke(this, progressData); } else { // Create the directory structure where the file will be downloaded var contentFilePath = GetUpdateFilePath(file); var contentFileDirectory = Path.GetDirectoryName(contentFilePath); if (!Directory.Exists(contentFileDirectory)) { Directory.CreateDirectory(contentFileDirectory); } // Download the file (or resume and interrupted download) contentDownloader.DownloadToFile(GetUpdateFilePath(file), file, cancellationSource.Token); progressData.CurrentOperation = OperationType.DownloadFileEnd; Progress?.Invoke(this, progressData); progressData.CurrentOperation = OperationType.HashFileStart; Progress?.Invoke(this, progressData); // Check the hash; must match the strongest hash specified in the update metadata if (hashChecker.Check(file, contentFilePath)) { var markerFile = File.Create(GetUpdateFileMarkerPath(file)); markerFile.Dispose(); } progressData.CurrentOperation = OperationType.HashFileEnd; Progress?.Invoke(this, progressData); } } }
/// <summary> /// Downloads the specified URL to the destination file stream /// </summary> /// <param name="destination">The file stream to write content to</param> /// <param name="updateFile">The update to download</param> /// <param name="startOffset">Offset to resume download at</param> /// <param name="cancellationToken">Cancellation token</param> public void DownloadToStream( Stream destination, UpdateFile updateFile, long startOffset, CancellationToken cancellationToken) { var progress = new ContentOperationProgress() { File = updateFile, Current = startOffset, Maximum = (long)updateFile.Size, CurrentOperation = OperationType.DownloadFileProgress }; // Validate starting offset if (startOffset >= (long)updateFile.Size) { throw new Exception($"Start offset {startOffset} cannot be greater than expected file size {updateFile.Size}"); } var url = updateFile.DownloadUrl; using (var client = new HttpClient()) { var fileSizeOnServer = GetFileSizeOnServer(client, url, cancellationToken); // Make sure our size matches the server's size if (fileSizeOnServer != (long)updateFile.Size) { throw new Exception($"File size mismatch. Expected {updateFile.Size}, server advertised {fileSizeOnServer}"); } // Build the range request for the download using (var updateRequest = new HttpRequestMessage { RequestUri = new Uri(url), Method = HttpMethod.Get }) { updateRequest.Headers.Range = new RangeHeaderValue((long)startOffset, (long)fileSizeOnServer - 1); // Stream the file to disk using (HttpResponseMessage response = client .SendAsync(updateRequest, HttpCompletionOption.ResponseHeadersRead, cancellationToken) .GetAwaiter() .GetResult()) { if (response.IsSuccessStatusCode) { using (Stream streamToReadFrom = response.Content.ReadAsStreamAsync().GetAwaiter().GetResult()) { // Read in chunks while not at the end and cancellation was not requested byte[] readBuffer = new byte[2097152 * 5]; var readBytesCount = streamToReadFrom.Read(readBuffer, 0, readBuffer.Length); while (!cancellationToken.IsCancellationRequested && readBytesCount > 0) { destination.Write(readBuffer, 0, readBytesCount); progress.Current += readBytesCount; OnDownloadProgress?.Invoke(this, progress); readBytesCount = streamToReadFrom.Read(readBuffer, 0, readBuffer.Length); } } } else { throw new Exception($"Failed to get content of update from {url}: {response.ReasonPhrase}"); } } } } }
/// <summary> /// Checks that the hash of a file matches the value specified in the update file metadata /// </summary> /// <param name="updateFile">The update file object that contains the expected checksums</param> /// <param name="filePath">The path to the file to checksum</param> /// <returns>The string representatin of the hash</returns> public bool Check(UpdateFile updateFile, string filePath) { byte[] readAheadBuffer, buffer; int readAheadBytesRead, bytesRead; int bufferSize = 512 * 1024; // Pick the stronges hash algorithm available HashAlgorithm hashAlgorithm; UpdateFileDigest targetDigest; if ((targetDigest = updateFile.Digests.Find(d => d.Algorithm.Equals("SHA512"))) != null) { hashAlgorithm = new SHA512Managed(); } else if ((targetDigest = updateFile.Digests.Find(d => d.Algorithm.Equals("SHA256"))) != null) { hashAlgorithm = new SHA256Managed(); } else if ((targetDigest = updateFile.Digests.Find(d => d.Algorithm.Equals("SHA1"))) != null) { hashAlgorithm = new SHA1Managed(); } else { throw new Exception($"No supported hashing algorithms found for update file {updateFile.FileName}"); } readAheadBuffer = new byte[bufferSize]; buffer = new byte[bufferSize]; // Hash the file contents using (var fileStream = File.Open(filePath, FileMode.Open, FileAccess.Read)) { var progress = new ContentOperationProgress() { File = updateFile, Current = 0, Maximum = (long)updateFile.Size, CurrentOperation = OperationType.HashFileProgress }; readAheadBytesRead = fileStream.Read(readAheadBuffer, 0, readAheadBuffer.Length); do { byte[] tempBuffer; bytesRead = readAheadBytesRead; tempBuffer = buffer; buffer = readAheadBuffer; readAheadBuffer = tempBuffer; readAheadBytesRead = fileStream.Read(readAheadBuffer, 0, readAheadBuffer.Length); if (readAheadBytesRead == 0) { hashAlgorithm.TransformFinalBlock(buffer, 0, bytesRead); progress.Current += bytesRead; } else { hashAlgorithm.TransformBlock(buffer, 0, bytesRead, buffer, 0); progress.Current += bytesRead; } OnHashingProgress?.Invoke(this, progress); } while (readAheadBytesRead != 0); // Check that actual hash matches the expected value var actualHash = hashAlgorithm.Hash; var expectedHash = Convert.FromBase64String(targetDigest.DigestBase64); return(actualHash.SequenceEqual(expectedHash)); } }