public override async Task ExecuteAsync(IOperationExecutionContext context) { if (!string.IsNullOrWhiteSpace(this.SourceServerName) && string.IsNullOrWhiteSpace(this.SourceDirectory)) { this.LogError("If SourceServer is specified, SourceDirectory must also be specified."); return; } var sourceAgent = context.Agent; var targetAgent = context.Agent; if (!string.IsNullOrWhiteSpace(this.SourceServerName)) { this.LogDebug($"Attempting to resolve source server \"{this.SourceServerName}\"..."); sourceAgent = await context.GetAgentAsync(this.SourceServerName).ConfigureAwait(false); } else { this.LogDebug("Using default source server."); } if (!string.IsNullOrWhiteSpace(this.TargetServerName)) { this.LogDebug($"Attempting to resolve target server \"{this.TargetServerName}\"..."); targetAgent = await context.GetAgentAsync(this.TargetServerName).ConfigureAwait(false); } else { this.LogDebug("Using default target server."); } if (sourceAgent == null) { this.LogError("Source server was not specified and there is no server in the current context."); return; } if (targetAgent == null) { this.LogError("Target server was not specified and there is no server in the current context."); return; } var sourceFileOps = await sourceAgent.GetServiceAsync <IFileOperationsExecuter>().ConfigureAwait(false); var targetFileOps = await targetAgent.GetServiceAsync <IFileOperationsExecuter>().ConfigureAwait(false); var sourceDirectory = this.SourceDirectory ?? context.WorkingDirectory; if (SDK.ProductName == "BuildMaster") { if (sourceDirectory.StartsWith("~\\") || sourceDirectory.StartsWith("~/")) { sourceDirectory = sourceFileOps.CombinePath( GetBuildMasterExecutionBaseWorkingDirectory(sourceFileOps, context), sourceDirectory.Substring(2) ); } } this.LogDebug("Source directory: " + sourceDirectory); this.LogDebug("Getting source file list..."); var sourceItems = await sourceFileOps.GetFileSystemInfosAsync(sourceDirectory, new MaskingContext(this.Includes, this.Excludes)).ConfigureAwait(false); var targetDirectory = this.TargetDirectory ?? context.WorkingDirectory; if (SDK.ProductName == "BuildMaster") { if (targetDirectory.StartsWith("~\\") || targetDirectory.StartsWith("~/")) { targetDirectory = targetFileOps.CombinePath( GetBuildMasterExecutionBaseWorkingDirectory(targetFileOps, context), targetDirectory.Substring(2) ); } } this.LogDebug("Target directory: " + targetDirectory); if (!PathEx.IsPathRooted(targetDirectory)) { this.LogError("Target directory must be rooted."); return; } if (this.VerboseLogging) { this.LogDebug($"Ensuring that {targetDirectory} exists..."); } await targetFileOps.CreateDirectoryAsync(targetDirectory); this.LogDebug("Getting target file list..."); var targetItems = targetFileOps .GetFileSystemInfos(targetDirectory, new MaskingContext(this.Includes, this.Excludes)) .ToDictionary(f => f.FullName.Replace('\\', '/')); var sourcePath = sourceDirectory.TrimEnd('/', '\\'); var targetPath = targetDirectory; int filesCopied = 0; int directoriesCopied = 0; int filesDeleted = 0; int directoriesDeleted = 0; var sourceFiles = sourceItems.OfType <SlimFileInfo>().ToList(); var sourceDirs = sourceItems.OfType <SlimDirectoryInfo>().ToList(); Interlocked.Exchange(ref this.totalBytes, sourceFiles.Sum(f => f.Size)); Func <SlimFileInfo, Task> transferFile = async(file) => { var targetFileName = PathEx.Combine(targetPath, file.FullName.Substring(sourcePath.Length).TrimStart('/', '\\')).Replace(sourceFileOps.DirectorySeparator, targetFileOps.DirectorySeparator); if (this.VerboseLogging) { this.LogDebug($"Copying {file.FullName} to {targetFileName}..."); } try { await this.TransferFileAsync(sourceFileOps, file, targetFileOps, targetItems.GetValueOrDefault(targetFileName.Replace('\\', '/')), PathEx.GetDirectoryName(targetFileName)).ConfigureAwait(false); Interlocked.Increment(ref filesCopied); } catch (Exception ex) { this.LogError($"Cannot copy {file.FullName}: {ex.Message}"); } Interlocked.Add(ref this.bytesCopied, file.Size); }; int batches = sourceFiles.Count / this.BatchSize; for (int batch = 0; batch < batches; batch++) { var tasks = new Task[this.BatchSize]; for (int i = 0; i < this.BatchSize; i++) { var file = sourceFiles[batch * this.BatchSize + i]; tasks[i] = transferFile(file); } await Task.WhenAll(tasks).ConfigureAwait(false); } if (batches * this.BatchSize != sourceFiles.Count) { var remaining = new Task[sourceFiles.Count % this.BatchSize]; for (int i = batches * this.BatchSize; i < sourceFiles.Count; i++) { var file = sourceFiles[i]; remaining[i % this.BatchSize] = transferFile(file); } await Task.WhenAll(remaining).ConfigureAwait(false); } foreach (var dir in sourceDirs) { var targetDir = PathEx.Combine(targetPath, dir.FullName.Substring(sourcePath.Length).TrimStart('/', '\\')).Replace(sourceFileOps.DirectorySeparator, targetFileOps.DirectorySeparator); if (this.VerboseLogging) { this.LogDebug($"Creating directory {targetDir}..."); } await targetFileOps.CreateDirectoryAsync(targetDir).ConfigureAwait(false); directoriesCopied++; } if (this.DeleteTarget) { var sourceItems2 = sourceItems.Select(f => f.FullName.Substring(sourcePath.Length).TrimStart('/', '\\').Replace('\\', '/')).ToHashSet(StringComparer.OrdinalIgnoreCase); foreach (var target in targetItems.Values) { var relativeItemPath = target.FullName.Substring(targetPath.Length).TrimStart('/', '\\'); if (!sourceItems2.Contains(relativeItemPath.Replace('\\', '/'))) { if (target is SlimFileInfo) { if (this.VerboseLogging) { this.LogDebug($"Deleting {target.FullName}..."); } await targetFileOps.DeleteFileAsync(target.FullName).ConfigureAwait(false); filesDeleted++; } else { if (this.VerboseLogging) { this.LogDebug($"Deleting directory {target.FullName}..."); } await targetFileOps.DeleteDirectoriesAsync(new[] { target.FullName }).ConfigureAwait(false); directoriesDeleted++; } } } } this.LogDebug($"Copied {filesCopied} files, deleted {filesDeleted} files and {directoriesDeleted} directories over {directoriesCopied} directories."); }