Skip to content

Commit 33e3875

Browse files
author
Bruce Haley
committed
Prevent files with duplicate names overwriting each other.
1 parent e8bda53 commit 33e3875

File tree

1 file changed

+84
-14
lines changed

1 file changed

+84
-14
lines changed

ExportPipelineDefinitions/Program.cs

Lines changed: 84 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ class Program
4343
static string domain = buildDomain;
4444
static bool isYamlPipeline = false;
4545
static List<string> yamlFileNames = new List<string>();
46+
// For holding duplicate template file names found in yamlFileNames. Allows avoiding file overwrites.
47+
static HashSet<string> yamlFileNameDuplicates = new HashSet<string>();
4648

4749
public class BuildDef
4850
{
@@ -164,7 +166,7 @@ public static async Task PopulateProjectList()
164166
// Documentation: https://docs.microsoft.com/en-us/rest/api/azure/devops/?view=azure-devops-server-rest-5.0
165167
// https://docs.microsoft.com/en-us/rest/api/azure/devops/core/projects/list?view=azure-devops-rest-5.1
166168

167-
string restUrl = String.Format("https://{0}/{1}/_apis/projects", domain, organization);
169+
string restUrl = string.Format("https://{0}/{1}/_apis/projects", domain, organization);
168170
Console.WriteLine("Getting build definitions from " + restUrl + "\n");
169171

170172
using (HttpClient client = new HttpClient())
@@ -207,7 +209,7 @@ public static async Task PopulateBuildDefinitionList(string project = "SDK_v4")
207209

208210
try
209211
{
210-
string restUrl = String.Format("https://{0}/{1}/{2}/_apis/build/definitions?includeAllProperties=true&api-version=5.0", buildDomain, organization, project);
212+
string restUrl = string.Format("https://{0}/{1}/{2}/_apis/build/definitions?includeAllProperties=true&api-version=5.0", buildDomain, organization, project);
211213

212214
using (HttpClient client = new HttpClient())
213215
{
@@ -249,7 +251,7 @@ public static async Task GetReleaseDefinitions(string project = "SDK_v4")
249251

250252
try
251253
{
252-
string restUrl = String.Format("https://{0}/{1}/{2}/_apis/release/definitions?api-version=5.0", releaseDomain, organization, project);
254+
string restUrl = string.Format("https://{0}/{1}/{2}/_apis/release/definitions?api-version=5.0", releaseDomain, organization, project);
253255

254256
using (HttpClient client = new HttpClient())
255257
{
@@ -295,7 +297,7 @@ public static async Task WriteDefinitionToFile(string project, string definition
295297

296298
try
297299
{
298-
string restUrl = String.Format("https://{0}/{1}/{2}/_apis/{3}/definitions/{4}?api-version=5.0",
300+
string restUrl = string.Format("https://{0}/{1}/{2}/_apis/{3}/definitions/{4}?api-version=5.0",
299301
domain, organization, project, definitionType, buildDef.id);
300302

301303
using (HttpClient client = new HttpClient())
@@ -357,6 +359,7 @@ public static async Task WriteDefinitionToFile(string project, string definition
357359
public static bool CheckForYamlPipeline(JObject json)
358360
{
359361
yamlFileNames.Clear();
362+
yamlFileNameDuplicates.Clear();
360363

361364
if (json != null && json["process"] != null && json["process"]["yamlFilename"] != null)
362365
{
@@ -367,10 +370,14 @@ public static bool CheckForYamlPipeline(JObject json)
367370
return false;
368371
}
369372

373+
/// <summary>
374+
/// Download the root .yml file, that is, the file referenced in the definition json.
375+
/// Then recursively find all template references and download those .yml template files.
376+
/// </summary>
377+
/// <param name="json"></param>
378+
/// <param name="directory"></param>
370379
public static void DownloadYamlFilesToDirectory(JObject json, string directory)
371380
{
372-
// Download the one .yml file referenced in the definition json.
373-
// Then walk the calling chain, recursively downloading any other .yml files.
374381
if (json != null && json["repository"] != null && json["repository"]["properties"] != null)
375382
{
376383
// Get the git repo URL for this Azure project.
@@ -396,7 +403,7 @@ public static void DownloadYamlFilesToDirectory(JObject json, string directory)
396403

397404
string githubFileUrl = string.Empty;
398405
string logIndent = string.Empty;
399-
string currentOffsetFromBaseUrl = String.Empty;
406+
string currentOffsetFromBaseUrl = string.Empty;
400407

401408
if (yamlFileNames.Count > 0)
402409
{
@@ -412,6 +419,7 @@ public static void DownloadYamlFilesToDirectory(JObject json, string directory)
412419
// Look for template references to other .yml files. Add them to yamlFileNames list.
413420
// Loop.
414421
string filePath = yamlFileNames.FirstOrDefault();
422+
415423
currentOffsetFromBaseUrl = filePath.Contains('/') ? filePath.Substring(0, filePath.LastIndexOf('/')) : "";
416424

417425
if (string.IsNullOrWhiteSpace(githubFileUrl))
@@ -421,9 +429,24 @@ public static void DownloadYamlFilesToDirectory(JObject json, string directory)
421429
logIndent = " "; // 8 spaces
422430
}
423431

424-
string fileName = githubFileUrl.Substring(githubFileUrl.LastIndexOf('/') + 1);
425-
string outputFilePath = $"{directory}\\{fileName}";
426-
bool succeeded = DownloadFileFromGithub(githubFileUrl, outputFilePath, logIndent);
432+
// Check whether this file has the same name as another. If so, it wil need its own directory to avoid a file overwrite.
433+
string outputFilePath;
434+
string fileName;
435+
string duplicateFileName = yamlFileNameDuplicates.FirstOrDefault(x => filePath.EndsWith(x, StringComparison.OrdinalIgnoreCase));
436+
if (!string.IsNullOrWhiteSpace(duplicateFileName))
437+
{
438+
// It does. Create a folder for it.
439+
fileName = duplicateFileName.Replace('/', '\\');
440+
string newDir = $"{directory}\\{fileName.Substring(0, fileName.LastIndexOf('\\'))}";
441+
System.IO.Directory.CreateDirectory(newDir);
442+
}
443+
else
444+
{
445+
fileName = githubFileUrl.Substring(githubFileUrl.LastIndexOf('/') + 1);
446+
}
447+
outputFilePath = $"{directory}\\{fileName}";
448+
449+
bool succeeded = DownloadFileFromGithub(githubFileUrl, outputFilePath, fileName, logIndent);
427450
yamlFileNames.RemoveAt(0);
428451

429452
if (succeeded)
@@ -478,7 +501,7 @@ private static string GetGithubRepoUrlMinusPath(JObject json)
478501
return repoUrl;
479502
}
480503

481-
private static bool DownloadFileFromGithub(string githubFileUrl, string outputFilePath, string logIndent)
504+
private static bool DownloadFileFromGithub(string githubFileUrl, string outputFilePath, string fileName, string logIndent)
482505
{
483506
// https://gist.github.com/EvanSnapp/ddf7f7f793474ea9631cbc0960295983
484507
// https://github.com/zayenCh/DownloadFile/blob/master/Downloader.cs
@@ -495,17 +518,16 @@ private static bool DownloadFileFromGithub(string githubFileUrl, string outputFi
495518
{
496519
string contents = webClient.DownloadString(new Uri(githubFileUrl));
497520
File.WriteAllText(outputFilePath, contents);
498-
string fileName = outputFilePath.Substring(outputFilePath.LastIndexOf("\\") + 1);
499521
Console.WriteLine($"{logIndent}* {fileName}");
500522
BuildCsvString(column, fileName);
501523

502524
return true;
503525
}
504526
catch (Exception ex)
505527
{
506-
string fileName = outputFilePath.Substring(outputFilePath.LastIndexOf("\\") + 1);
507528
Console.WriteLine($"{logIndent}* {fileName}**");
508529
BuildCsvString(column, fileName + "**");
530+
// We couldn't get the file contents, so write the error message to the file instead.
509531
string contents = $"* {ex.Message.ToString()}\r\n{githubFileUrl}";
510532
File.WriteAllText(outputFilePath, contents);
511533
if (ex.Message.ToString().Contains("(404)"))
@@ -535,12 +557,13 @@ private static void AddYamlTemplateReferencesFromFile(
535557
int index = name.IndexOf("#"); // Find and remove any other comment.
536558
if (index >= 0) name = name.Remove(index).TrimEnd();
537559
name = name.TrimStart('-').Replace(match, "").Trim(); // TrimStart() handles the case of "- template:".
538-
if (!String.IsNullOrWhiteSpace(name))
560+
if (!string.IsNullOrWhiteSpace(name))
539561
{
540562
// Normalize the relative path.
541563
name = Path.GetFullPath((Path.Combine("/", currentOffsetFromRootUrl, name))).Replace(@"C:\", "").TrimStart('\\').Replace(@"\", "/");
542564
if (!yamlFileNames.Contains(name))
543565
{
566+
AddToYamlFileNameDuplicates(name);
544567
yamlFileNames.Add(name);
545568
}
546569
}
@@ -550,6 +573,53 @@ private static void AddYamlTemplateReferencesFromFile(
550573
return;
551574
}
552575

576+
/// <summary>
577+
/// This checks filePath to see whether the file name duplicates a file name already in
578+
/// yamlFileNames. If so, it adds the two files to yamlFileNameDuplicates.
579+
/// For files in yamlFileNameDuplicates we make an exception to folder flattening and
580+
/// create separate download folders to prevent one file overwriting the other.
581+
/// </summary>
582+
/// <param name="filePath"></param>
583+
private static void AddToYamlFileNameDuplicates(string filePath)
584+
{
585+
if (yamlFileNames.Contains(filePath))
586+
{
587+
// This is a duplicate reference to the same file. We don't dedupe these. Skip it.
588+
return;
589+
}
590+
591+
string[] filePathArray = filePath.Split('/');
592+
filePathArray = filePathArray.Reverse().ToArray();
593+
594+
foreach (string yamlFileName in yamlFileNames)
595+
{
596+
string[] yamlFileNameArray = yamlFileName.Split('/');
597+
yamlFileNameArray = yamlFileNameArray.Reverse().ToArray();
598+
599+
for (int i = 0; i < filePathArray.Length && i < yamlFileNameArray.Length; i++)
600+
{
601+
if (!filePathArray[i].Equals(yamlFileNameArray[i], StringComparison.OrdinalIgnoreCase))
602+
{
603+
if (i == 0) break; // Names are not dupes.
604+
605+
string outFilePath = "";
606+
string outyamlPath = "";
607+
int j = i;
608+
while (j >= 0)
609+
{
610+
outFilePath += filePathArray[j] + "/";
611+
outyamlPath += yamlFileNameArray[j] + "/";
612+
j--;
613+
}
614+
615+
yamlFileNameDuplicates.Add(outFilePath.Trim('/'));
616+
yamlFileNameDuplicates.Add(outyamlPath.Trim('/'));
617+
return; // No more dupes that are not accounted for, so quit.
618+
}
619+
}
620+
}
621+
}
622+
553623
private static int CompareProjects(Proj x, Proj y)
554624
{
555625
if (x == null)

0 commit comments

Comments
 (0)