/// <summary> /// Performs a transfer of the specified file to the target providers. /// </summary> /// <param name="File"></param> /// <param name="CancelToken"></param> public async Task TransferAsync(BackupFile File, SourceLocation Source, CancellationToken CancelToken) { if (File == null) { throw new ArgumentNullException(nameof(File)); } if (Source == null) { throw new ArgumentNullException(nameof(Source)); } if (CancelToken == null) { throw new ArgumentNullException(nameof(CancelToken)); } Logger.WriteTraceMessage(string.Format("Starting transfer operation for file: {0}", File.ToString()), InstanceID); try { // step 1: safety checks. // bail out if the file is missing or contents are empty. var info = new FileInfo(File.FullSourcePath); if (!info.Exists) { Logger.WriteTraceMessage(string.Format("Unable to backup file ({0}). It has been deleted or is no longer accessible since it was scanned.", File.FullSourcePath), InstanceID); await Database.DeleteBackupFileAsync(File).ConfigureAwait(false); await Database.RemoveFileFromBackupQueueAsync(File).ConfigureAwait(false); return; } if (info.Length == 0) { var message = string.Format("Unable to backup file ({0}). It is empty (has no contents).", File.FullSourcePath); Logger.WriteTraceMessage(message, InstanceID); await Database.SetBackupFileAsFailedAsync(File, message).ConfigureAwait(false); await Database.RemoveFileFromBackupQueueAsync(File).ConfigureAwait(false); return; } // cancel if the caller is shutting down. // add these in a few different places to ensure we aren't holding the threads open too long. CancelToken.ThrowIfCancellationRequested(); // step 2: open up a filestream to the specified file. // use a read-only lock: this prevents the file from being modified while this lock is open. // but others can still open for read. using (FileStream fs = new FileStream(File.FullSourcePath, FileMode.Open, FileAccess.Read, FileShare.Read)) { // step 3: calculate and save the hash. CancelToken.ThrowIfCancellationRequested(); await UpdateFileHashInDatabaseAsync(File, fs).ConfigureAwait(false); // step 4: see if this file is already on the destination target provider(s). // this avoids resending the file if for some reason the client DB/states got wiped out. CancelToken.ThrowIfCancellationRequested(); await UpdateFileCopyStateIfFileAlreadyExistsAtProvidersAsync(File, Source, fs).ConfigureAwait(false); // step 5: while the file has data that needs to be transferred- transfer it. // this includes transferring to each potential target that needs this same file block. while (File.HasDataToTransfer()) { // step 5A: generate the next transfer data block. CancelToken.ThrowIfCancellationRequested(); var payload = File.GenerateNextTransferPayload(fs, Hasher); // step 5B: send the transfer payload. CancelToken.ThrowIfCancellationRequested(); await SendTransferPayloadToFileTargetsAsync(File, Source, payload, CancelToken).ConfigureAwait(false); } // no more data to transfer, remove the file from the backup queue. await Database.RemoveFileFromBackupQueueAsync(File).ConfigureAwait(false); } } catch (OperationCanceledException) { // the caller has requested that we stop. Logger.WriteTraceWarning("An in-progress file transfer has been cancelled by request of the Backup Engine. It will be resumed the next time the engine starts up.", InstanceID); } catch (Exception ex) { var message = "An error occurred while preparing to transfer a file."; Logger.WriteTraceError(message, ex, Logger.GenerateFullContextStackTrace(), InstanceID); await Database.SetBackupFileAsFailedAsync(File, (message + ex.ToString())).ConfigureAwait(false); await Database.RemoveFileFromBackupQueueAsync(File).ConfigureAwait(false); } }