Esempio n. 1
0
        /// <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;
 }
Esempio n. 3
0
 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);
 }
Esempio n. 4
0
		/// <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);
			}
		}
Esempio n. 5
0
		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);
			}
		}
Esempio n. 6
0
		/// <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);
		}
Esempio n. 7
0
		/// <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();
			}
		}
Esempio n. 8
0
		/// <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()
				});
			}
		}
Esempio n. 9
0
		/// <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();
			}
		}
Esempio n. 10
0
		/// <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();
			}
		}
Esempio n. 11
0
		/// <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);
			}
		}
Esempio n. 12
0
		/// <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);
			}
		}
Esempio n. 13
0
		/// <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.");
				}
			}
		}