/// <summary> /// Ensures that we have a feedback channel, creating a dummy one if needed. /// Avoids bothersome conditional logic related to feedback - we always provide it. /// </summary> public static void EnsureFeedbackChannel(ref IFeedbackChannel feedbackChannel) { if (feedbackChannel != null) return; feedbackChannel = DummyFeedbackChannel.Default; }
public DeployService(ILogger <DeployService> logger, IFeedbackChannel feedbackChannel, IScriptManager scriptManager) { _logger = logger; _feedbackChannel = feedbackChannel; _config = new DeployServiceConfiguration(); _scriptManager = scriptManager; }
public static void LogMethodCall(string methodName, IFeedbackChannel feedbackChannel, CancellationToken cancellationToken) { Debug.WriteLine("{0} called. Has feedback channel: {1} Is cancelable: {2}", methodName, feedbackChannel != null, cancellationToken.CanBeCanceled); }
/// <summary> /// Uploads and downloads random bytes of the specified length and verifies content integrity. /// Useful to ensure that we do not freak out with some special size due to math errors. /// </summary> private async Task TestFileUploadAndDownload(int size, FilesystemSnapshot filesystem, IFeedbackChannel feedback) { var data = TestHelper.GetRandomBytes(size); // Just stick it in the root, do not care about what already exists there. CloudItem file; using (var stream = new MemoryStream(data)) file = await filesystem.Files.NewFileAsync(size.ToString(), stream, feedback); var target = Path.GetTempFileName(); try { await file.DownloadContentsAsync(target, feedback); using (var contents = File.OpenRead(target)) using (var expectedContents = new MemoryStream(data)) TestHelper.AssertStreamsAreEqual(expectedContents, contents); } finally { File.Delete(target); } }
private async Task DeleteUnwantedItems(CloudItem rootDirectory, ICollection<OpaqueID> wantedItems, IFeedbackChannel feedback) { foreach (var ci in rootDirectory.Children) { if (!wantedItems.Contains(ci.ID)) { await ci.DeleteAsync(feedback); continue; } // If it is a wanted item, it might not have wanted children, so delete them. if (ci.Type == ItemType.Folder) await DeleteUnwantedItems(ci, wantedItems, feedback); } }
/// <summary> /// Brings the two test accounts to the initial state. /// </summary> /// <remarks> /// Account 1 contents: /// Files /// - Folder1 /// -- Folder2 /// --- EmptyFile /// -- SmallFile /// - BigFile /// - MediumFile /// Contacts /// /// Account 2 contents: /// Files /// Contacts /// - Account 1 /// </remarks> public async Task BringToInitialState(IFeedbackChannel feedback) { var client1 = new MegaClient(Email1, Password1); var client2 = new MegaClient(Email2, Password2); var filesystem1 = await client1.GetFilesystemSnapshotAsync(feedback); // Got folders? var folder1 = filesystem1.Files.Children .FirstOrDefault(ci => ci.Type == ItemType.Folder && ci.Name == "Folder1"); CloudItem folder2 = null; if (folder1 != null) folder2 = folder1.Children .FirstOrDefault(ci => ci.Type == ItemType.Folder && ci.Name == "Folder2"); // Make folders, if needed. if (folder1 == null) folder1 = await filesystem1.Files.NewFolderAsync("Folder1", feedback); if (folder2 == null) folder2 = await folder1.NewFolderAsync("Folder2", feedback); // Got files? var bigFile = BigFile.TryFind(filesystem1); var mediumFile = MediumFile.TryFind(filesystem1); var smallFile = SmallFile.TryFind(filesystem1); var emptyFile = EmptyFile.TryFind(filesystem1); // Then upload the new files. if (emptyFile == null || emptyFile.Parent != folder2) using (var stream = OpenTestDataFile(EmptyFile.Name)) emptyFile = await folder2.NewFileAsync(EmptyFile.Name, stream, feedback); if (smallFile == null || smallFile.Parent != folder1) using (var stream = OpenTestDataFile(SmallFile.Name)) smallFile = await folder1.NewFileAsync(SmallFile.Name, stream, feedback); if (mediumFile == null || mediumFile.Parent != filesystem1.Files) using (var stream = OpenTestDataFile(MediumFile.Name)) mediumFile = await filesystem1.Files.NewFileAsync(MediumFile.Name, stream, feedback); if (bigFile == null || bigFile.Parent != filesystem1.Files) using (var stream = OpenTestDataFile(BigFile.Name)) bigFile = await filesystem1.Files.NewFileAsync(BigFile.Name, stream, feedback); // Delete all items that we do not care about for account 1. var goodItems = new[] { folder1.ID, folder2.ID, bigFile.ID, mediumFile.ID, smallFile.ID, emptyFile.ID }; await DeleteUnwantedItems(filesystem1.Files, goodItems, feedback); await DeleteUnwantedItems(filesystem1.Trash, goodItems, feedback); // Delete all files and folders for account 2. var filesystem2 = await client2.GetFilesystemSnapshotAsync(feedback); foreach (var item in filesystem2.AllItems.Where(ci => ci.Type == ItemType.File || ci.Type == ItemType.Folder)) await item.DeleteAsync(feedback); // Delete all contacts. foreach (var contact in await client1.GetContactListSnapshotAsync(feedback)) await contact.RemoveAsync(feedback); foreach (var contact in await client2.GetContactListSnapshotAsync(feedback)) await contact.RemoveAsync(feedback); // Add account 1 as contact to account 2. await client2.AddContactAsync(Email1, feedback); }
/// <summary> /// Saves the name and any other attributes after they have been locally modified. /// </summary> /// <param name="feedbackChannel">Allows you to receive feedback about the operation while it is running.</param> /// <param name="cancellationToken">Allows you to cancel the operation.</param> public async Task SaveAsync(IFeedbackChannel feedbackChannel = null, CancellationToken cancellationToken = default(CancellationToken)) { PatternHelper.LogMethodCall("SaveAsync", feedbackChannel, cancellationToken); PatternHelper.EnsureFeedbackChannel(ref feedbackChannel); if (Type != ItemType.File && Type != ItemType.Folder) throw new InvalidOperationException("You can only modify files and folders."); using (await _client.AcquireLock(feedbackChannel, cancellationToken)) { var itemKey = _client.DecryptItemKey(EncryptedKeys); var attributesKey = Algorithms.DeriveNodeAttributesKey(itemKey); // Encrypt the item with the current account's AES key. var encryptedKey = Algorithms.EncryptKey(itemKey, _client.AesKey); await _client.ExecuteCommandInternalAsync<SuccessResult>(feedbackChannel, cancellationToken, new SetItemAttributesCommand { ClientInstanceID = _client._clientInstanceID, ItemID = ID, EncryptedItemKey = encryptedKey, Attributes = Attributes.SerializeAndEncrypt(attributesKey) }); _client.InvalidateFilesystemInternal(); } }
/// <summary> /// Sends the item to a contact's Inbox folder. If the item is a folder, the items are sent recursively. /// </summary> /// <param name="contact">Recipient of the item.</param> /// <param name="feedbackChannel">Allows you to receive feedback about the operation while it is running.</param> /// <param name="cancellationToken">Allows you to cancel the operation.</param> public async Task SendToContactAsync(Contact contact, IFeedbackChannel feedbackChannel = null, CancellationToken cancellationToken = default(CancellationToken)) { Argument.ValidateIsNotNull(contact, "contact"); if (Type != ItemType.File && Type != ItemType.Folder) throw new InvalidOperationException("You can only send files or folders."); PatternHelper.LogMethodCall("SendToContactAsync", feedbackChannel, cancellationToken); PatternHelper.EnsureFeedbackChannel(ref feedbackChannel); var itemsToSend = GetDescendants(); itemsToSend.Add(this); using (await _client.AcquireLock(feedbackChannel, cancellationToken)) { var contactPublicKey = await _client.GetAccountPublicKeyInternalAsync(contact.ID, feedbackChannel, cancellationToken); var newItems = new List<NewItemsCommand.NewItem>(); foreach (var item in itemsToSend) { var itemKey = _client.DecryptItemKey(item.EncryptedKeys); var attributesKey = Algorithms.DeriveNodeAttributesKey(itemKey); var contactEncryptedKey = Algorithms.RsaEncrypt(itemKey, contactPublicKey); var newItem = new NewItemsCommand.NewItem { ItemContentsReference = item.ID.BinaryData, Type = item.TypeID, EncryptedItemKey = contactEncryptedKey, Attributes = item.Attributes.SerializeAndEncrypt(attributesKey) }; // If this is not the current item, mark its parent reference, to preserve hierarchy. if (item != this) newItem.ParentItemContentsReference = item.Parent.ID.BinaryData; newItems.Add(newItem); feedbackChannel.WriteVerbose("Sending to contact: {0}", item.Name); } await _client.ExecuteCommandInternalAsync<NewItemsResult>(feedbackChannel, cancellationToken, new NewItemsCommand { ClientInstanceID = _client._clientInstanceID, ParentID = contact.ID, Items = newItems.ToArray() }); } }
/// <summary> /// Deletes the item. /// </summary> /// <param name="feedbackChannel">Allows you to receive feedback about the operation while it is running.</param> /// <param name="cancellationToken">Allows you to cancel the operation.</param> public async Task DeleteAsync(IFeedbackChannel feedbackChannel = null, CancellationToken cancellationToken = default(CancellationToken)) { if (Type != ItemType.File && Type != ItemType.Folder) throw new InvalidOperationException("You can only delete files or folders."); PatternHelper.LogMethodCall("DeleteAsync", feedbackChannel, cancellationToken); PatternHelper.EnsureFeedbackChannel(ref feedbackChannel); using (await _client.AcquireLock(feedbackChannel, cancellationToken)) { feedbackChannel.Status = "Deleting item: " + Name; try { await _client.ExecuteCommandInternalAsync<SuccessResult>(feedbackChannel, cancellationToken, new DeleteItemCommand { ClientInstanceID = _client._clientInstanceID, ItemID = ID, }); } catch (ItemNotFoundException) { // Already deleted? Oh happy days! } catch (AccessDeniedException) { // Also happens sometimes when the item has already been deleted. // This has happened if you delete a parent immediately before deleting the child. } _client.InvalidateFilesystemInternal(); } }
/// <summary> /// Moves the item under another item. This operation is valid for files and folders. /// The parent is not updated in any existing filesystem snapshot. /// </summary> /// <param name="newParent">The new parent of the item.</param> /// <param name="feedbackChannel">Allows you to receive feedback about the operation while it is running.</param> /// <param name="cancellationToken">Allows you to cancel the operation.</param> public async Task MoveAsync(CloudItem newParent, IFeedbackChannel feedbackChannel = null, CancellationToken cancellationToken = default(CancellationToken)) { Argument.ValidateIsNotNull(newParent, "newParent"); if (Type != ItemType.File && Type != ItemType.Folder) throw new InvalidOperationException("You can only move files or folders."); if (!newParent.IsContainer) throw new InvalidOperationException("The specified destination cannot contain other items."); PatternHelper.LogMethodCall("MoveAsync", feedbackChannel, cancellationToken); PatternHelper.EnsureFeedbackChannel(ref feedbackChannel); using (await _client.AcquireLock(feedbackChannel, cancellationToken)) { await _client.ExecuteCommandInternalAsync<SuccessResult>(feedbackChannel, cancellationToken, new MoveItemCommand { ClientInstanceID = _client._clientInstanceID, ItemID = ID, ParentID = newParent.ID }); _client.InvalidateFilesystemInternal(); } }
/// <summary> /// Creates a new file in the current folder. This operation is only valid for folders. /// The file is not added to any existing filesystem snapshot, but you can use the returned object to operate on it. /// </summary> /// <param name="name">The name of the file to add.</param> /// <param name="contents">A stream with the contents that the file will have.</param> /// <param name="feedbackChannel">Allows you to receive feedback about the operation while it is running.</param> /// <param name="cancellationToken">Allows you to cancel the operation.</param> public async Task<CloudItem> NewFileAsync(string name, Stream contents, IFeedbackChannel feedbackChannel = null, CancellationToken cancellationToken = default(CancellationToken)) { Argument.ValidateIsNotNullOrWhitespace(name, "name"); Argument.ValidateIsNotNull(contents, "contents"); if (name.IndexOfAny(new[] { '/', '\\' }) != -1) throw new ArgumentException("A file name cannot contain path separator characters.", "name"); if (!IsContainer) throw new InvalidOperationException("This item cannot contain child items."); if (!contents.CanRead) throw new ArgumentException("The contents stream is not readable.", "contents"); if (!contents.CanSeek) throw new ArgumentException("The contents stream is not seekable.", "contents"); PatternHelper.LogMethodCall("NewFileAsync", feedbackChannel, cancellationToken); PatternHelper.EnsureFeedbackChannel(ref feedbackChannel); using (await _client.AcquireLock(feedbackChannel, cancellationToken)) { feedbackChannel.Status = "Preparing for upload"; var beginUploadResult = await _client.ExecuteCommandInternalAsync<BeginUploadResult>(feedbackChannel, cancellationToken, new BeginUploadCommand { Size = contents.Length }); cancellationToken.ThrowIfCancellationRequested(); feedbackChannel.Status = "Uploading file contents"; Base64Data? completionToken = null; // Set when last chunk has been uploaded. var chunkSizes = Algorithms.MeasureChunks(contents.Length); var chunkCount = chunkSizes.Length; var chunkMacs = new byte[chunkCount][]; var dataKey = Algorithms.GetRandomBytes(16); var nonce = Algorithms.GetRandomBytes(8); // Limit number of chunks in flight at the same time. var concurrentUploadSemaphore = new SemaphoreSlim(4); // Only one file read operation can take place at a time. var concurrentReadSemaphore = new SemaphoreSlim(1); // For progress calculations. long completedBytes = 0; CancellationTokenSource chunkUploadsCancellationSource = new CancellationTokenSource(); var uploadTasks = new List<Task>(); for (int i = 0; i < chunkCount; i++) { int chunkIndex = i; long startOffset = chunkSizes.Take(i).Select(size => (long)size).Sum(); uploadTasks.Add(Task.Run(async delegate { var operationName = string.Format("Uploading chunk {0} of {1}", chunkIndex + 1, chunkSizes.Length); using (var chunkFeedback = feedbackChannel.BeginSubOperation(operationName)) { byte[] bytes = new byte[chunkSizes[chunkIndex]]; using (await SemaphoreLock.TakeAsync(concurrentUploadSemaphore)) { chunkUploadsCancellationSource.Token.ThrowIfCancellationRequested(); using (await SemaphoreLock.TakeAsync(concurrentReadSemaphore)) { chunkUploadsCancellationSource.Token.ThrowIfCancellationRequested(); chunkFeedback.Status = "Reading contents"; // Read in the raw bytes for this chunk. contents.Position = startOffset; contents.Read(bytes, 0, bytes.Length); } chunkFeedback.Status = "Encrypting contents"; byte[] chunkMac; Algorithms.EncryptNodeDataChunk(bytes, dataKey, nonce, out chunkMac, startOffset); chunkMacs[chunkIndex] = chunkMac; await RetryHelper.ExecuteWithRetryAsync(async delegate { chunkUploadsCancellationSource.Token.ThrowIfCancellationRequested(); chunkFeedback.Status = string.Format("Uploading {0} bytes", chunkSizes[chunkIndex]); var url = beginUploadResult.UploadUrl + "/" + startOffset; HttpResponseMessage response; using (var client = new HttpClient()) response = await client.PostAsyncCancellationSafe(url, new ByteArrayContent(bytes), chunkUploadsCancellationSource.Token); response.EnsureSuccessStatusCode(); var responseBody = await response.Content.ReadAsStringAsync(); // Result from last chunk is: base64-encoded completion handle to give to NewItemsCommand // Negative ASCII integer in case of error. Standard-ish stuff? // Empty is just OK but not last chunk. if (responseBody.StartsWith("[")) { // Error result! // Assuming it is formatted like this, I never got it to return an error result. // It always just hangs if I do anything funny... var errorResult = JObject.Parse(responseBody); Channel.ThrowOnFailureResult(errorResult); throw new ProtocolViolationException("Got an unexpected result from chunk upload: " + responseBody); } else if (!string.IsNullOrWhiteSpace(responseBody)) { // Completion token! completionToken = responseBody; } if (bytes.Length != chunkSizes[chunkIndex]) throw new MegaException(string.Format("Expected {0} bytes in chunk but got {1}.", chunkSizes[chunkIndex], bytes.Length)); }, ChunkUploadRetryPolicy, chunkFeedback, chunkUploadsCancellationSource.Token); } Interlocked.Add(ref completedBytes, chunkSizes[chunkIndex]); } }, chunkUploadsCancellationSource.Token)); } // Wait for all tasks to finish. Stop immediately on cancel or if any single task fails. while (uploadTasks.Any(d => !d.IsCompleted)) { feedbackChannel.Progress = Interlocked.Read(ref completedBytes) * 1.0 / contents.Length; Exception failureReason = null; if (cancellationToken.IsCancellationRequested) { failureReason = new OperationCanceledException(); } else { var failedTask = uploadTasks.FirstOrDefault(d => d.IsFaulted || d.IsCanceled); if (failedTask != null) { if (failedTask.Exception != null) failureReason = failedTask.Exception.GetBaseException(); else failureReason = new MegaException("The file could not be uploaded."); } } if (failureReason == null) { await Task.Delay(1000); continue; } chunkUploadsCancellationSource.Cancel(); feedbackChannel.Status = "Stopping upload due to subtask failure"; try { // Wait for all of the tasks to complete, just so we do not leave any dangling activities in the background. Task.WaitAll(uploadTasks.ToArray()); } catch { // It will throw something no notify us of the cancellation. Whatever, do not care. } // Rethrow the failure causing exception. ExceptionDispatchInfo.Capture(failureReason).Throw(); } if (!completionToken.HasValue) throw new ProtocolViolationException("Mega did not provide upload completion token."); feedbackChannel.Progress = 1; feedbackChannel.Progress = null; feedbackChannel.Status = "Creating filesystem entry"; var metaMac = Algorithms.CalculateMetaMac(chunkMacs, dataKey); var itemKey = Algorithms.CreateNodeKey(dataKey, nonce, metaMac); var attributesKey = Algorithms.DeriveNodeAttributesKey(itemKey); // Create the file from the uploaded data. var result = await _client.ExecuteCommandInternalAsync<NewItemsResult>(feedbackChannel, cancellationToken, new NewItemsCommand { ClientInstanceID = _client._clientInstanceID, ParentID = ID, Items = new[] { new NewItemsCommand.NewItem { Attributes = new ItemAttributes { { "n", name } }.SerializeAndEncrypt(attributesKey), Type = KnownItemTypes.File, EncryptedItemKey = Algorithms.EncryptKey(itemKey, _client.AesKey), ItemContentsReference = completionToken.Value } } }); _client.InvalidateFilesystemInternal(); return FromTemplate(result.Items.Single(), _client); } }
/// <summary> /// Creates a new folder in the current folder. This operation is only valid for folders. /// The folder is not added to any existing filesystem snapshot, but you can use the returned object to operate on it. /// </summary> /// <param name="name">The name of the folder.</param> /// <param name="feedbackChannel">Allows you to receive feedback about the operation while it is running.</param> /// <param name="cancellationToken">Allows you to cancel the operation.</param> public async Task<CloudItem> NewFolderAsync(string name, IFeedbackChannel feedbackChannel = null, CancellationToken cancellationToken = default(CancellationToken)) { Argument.ValidateIsNotNullOrWhitespace(name, "name"); if (name.IndexOfAny(new[] { '/', '\\' }) != -1) throw new ArgumentException("A folder name cannot contain path separator characters.", "name"); if (!IsContainer) throw new InvalidOperationException("This item cannot contain child items."); PatternHelper.LogMethodCall("NewFolderAsync", feedbackChannel, cancellationToken); PatternHelper.EnsureFeedbackChannel(ref feedbackChannel); using (await _client.AcquireLock(feedbackChannel, cancellationToken)) { feedbackChannel.Status = "Creating folder"; var itemKey = Algorithms.GetRandomBytes(16); var attributesKey = Algorithms.DeriveNodeAttributesKey(itemKey); var result = await _client.ExecuteCommandInternalAsync<NewItemsResult>(feedbackChannel, cancellationToken, new NewItemsCommand { ClientInstanceID = _client._clientInstanceID, ParentID = ID, Items = new[] { new NewItemsCommand.NewItem { Attributes = new ItemAttributes { { "n", name } }.SerializeAndEncrypt(attributesKey), Type = KnownItemTypes.Folder, EncryptedItemKey = Algorithms.EncryptKey(itemKey, _client.AesKey) } } }); _client.InvalidateFilesystemInternal(); return FromTemplate(result.Items.Single(), _client); } }
/// <summary> /// Downloads the contents of the item to the local filesystem. This operation is only valid for files. /// </summary> /// <param name="destinationPath">Path to the destination file that will contain the contents of this item.</param> /// <param name="feedbackChannel">Allows you to receive feedback about the operation while it is running.</param> /// <param name="cancellationToken">Allows you to cancel the operation.</param> public async Task DownloadContentsAsync(string destinationPath, IFeedbackChannel feedbackChannel = null, CancellationToken cancellationToken = default(CancellationToken)) { Argument.ValidateIsNotNullOrWhitespace(destinationPath, "destinationPath"); if (Type != ItemType.File) throw new InvalidOperationException("You can only download files."); PatternHelper.LogMethodCall("DownloadContentsAsync", feedbackChannel, cancellationToken); PatternHelper.EnsureFeedbackChannel(ref feedbackChannel); using (var file = File.Create(destinationPath)) { // If this file does not have any contents, there is nothing to download :) if (!HasContents) return; using (await _client.AcquireLock(feedbackChannel, cancellationToken)) { feedbackChannel.Status = "Requesting download URL"; var result = await _client.ExecuteCommandInternalAsync<GetDownloadLinkResult>(feedbackChannel, cancellationToken, new GetDownloadCommand { ItemID = ID }); feedbackChannel.Status = "Preparing for download"; var itemKey = _client.DecryptItemKey(EncryptedKeys); var dataKey = Algorithms.DeriveNodeDataKey(itemKey); var nonce = itemKey.Skip(16).Take(8).ToArray(); var metaMac = itemKey.Skip(24).Take(8).ToArray(); feedbackChannel.Status = "Pre-allocating file"; file.SetLength(result.Size); feedbackChannel.Status = "Downloading"; var chunkSizes = Algorithms.MeasureChunks(result.Size); var chunkCount = chunkSizes.Length; var chunkMacs = new byte[chunkCount][]; // Limit number of chunks in flight at the same time. var concurrentDownloadSemaphore = new SemaphoreSlim(4); // Only one file write operation can take place at a time. var concurrentWriteSemaphore = new SemaphoreSlim(1); // For progress calculations. long completedBytes = 0; CancellationTokenSource chunkDownloadsCancellationSource = new CancellationTokenSource(); // Get chunks in parallel. List<Task> chunkDownloads = new List<Task>(); for (int i = 0; i < chunkCount; i++) { int chunkIndex = i; long startOffset = chunkSizes.Take(i).Select(size => (long)size).Sum(); long endOffset = startOffset + chunkSizes[i]; // Each chunk is downloaded and processed by this separately. chunkDownloads.Add(Task.Run(async delegate { var operationName = string.Format("Downloading chunk {0} of {1}", chunkIndex + 1, chunkSizes.Length); using (var chunkFeedbackChannel = feedbackChannel.BeginSubOperation(operationName)) { byte[] bytes = null; using (await SemaphoreLock.TakeAsync(concurrentDownloadSemaphore)) { await RetryHelper.ExecuteWithRetryAsync(async delegate { chunkDownloadsCancellationSource.Token.ThrowIfCancellationRequested(); chunkFeedbackChannel.Status = string.Format("Downloading {0} bytes", chunkSizes[chunkIndex]); // Range is inclusive, so do -1 for the end offset. var url = result.DownloadUrl + "/" + startOffset + "-" + (endOffset - 1); HttpResponseMessage response; using (var client = new HttpClient()) response = await client.GetAsyncCancellationSafe(url, chunkDownloadsCancellationSource.Token); response.EnsureSuccessStatusCode(); bytes = await response.Content.ReadAsByteArrayAsync(); if (bytes.Length != chunkSizes[chunkIndex]) throw new MegaException(string.Format("Expected {0} bytes in chunk but got {1}.", chunkSizes[chunkIndex], bytes.Length)); }, ChunkDownloadRetryPolicy, chunkFeedbackChannel, chunkDownloadsCancellationSource.Token); } chunkDownloadsCancellationSource.Token.ThrowIfCancellationRequested(); // OK, got the bytes. Now decrypt them and calculate MAC. chunkFeedbackChannel.Status = "Decrypting"; byte[] chunkMac; Algorithms.DecryptNodeDataChunk(bytes, dataKey, nonce, out chunkMac, startOffset); chunkMacs[chunkIndex] = chunkMac; chunkDownloadsCancellationSource.Token.ThrowIfCancellationRequested(); // Now write to file. chunkFeedbackChannel.Status = "Writing to file"; using (await SemaphoreLock.TakeAsync(concurrentWriteSemaphore)) { file.Position = startOffset; file.Write(bytes, 0, bytes.Length); file.Flush(true); } Interlocked.Add(ref completedBytes, chunkSizes[chunkIndex]); } }, chunkDownloadsCancellationSource.Token)); } // Wait for all tasks to finish. Stop immediately on cancel or if any single task fails. while (chunkDownloads.Any(d => !d.IsCompleted)) { feedbackChannel.Progress = Interlocked.Read(ref completedBytes) * 1.0 / result.Size; Exception failureReason = null; if (cancellationToken.IsCancellationRequested) { failureReason = new OperationCanceledException(); } else { var failedTask = chunkDownloads.FirstOrDefault(d => d.IsFaulted || d.IsCanceled); if (failedTask != null) { if (failedTask.Exception != null) failureReason = failedTask.Exception.GetBaseException(); else failureReason = new MegaException("The file could not be downloaded."); } } if (failureReason == null) { await Task.Delay(1000); continue; } chunkDownloadsCancellationSource.Cancel(); feedbackChannel.Status = "Stopping download due to subtask failure"; try { // Wait for all of the tasks to complete, just so we do not leave any dangling activities in the background. Task.WaitAll(chunkDownloads.ToArray()); } catch { // It will throw something no notify us of the cancellation. Whatever, do not care. } // Rethrow the failure causing exception. ExceptionDispatchInfo.Capture(failureReason).Throw(); } feedbackChannel.Progress = 1; feedbackChannel.Status = "Verifying file"; // Verify meta-MAC. byte[] calculatedMetaMac = Algorithms.CalculateMetaMac(chunkMacs, dataKey); if (!metaMac.SequenceEqual(calculatedMetaMac)) throw new DataIntegrityException("File meta-MAC did not match expected value. File may have been corrupted during download."); } } }