diff --git a/AdvancedInstaller/AzureDevOpsBackup Installer.aip b/AdvancedInstaller/AzureDevOpsBackup Installer.aip index 27fc781..1383305 100644 --- a/AdvancedInstaller/AzureDevOpsBackup Installer.aip +++ b/AdvancedInstaller/AzureDevOpsBackup Installer.aip @@ -4,6 +4,7 @@ + @@ -17,6 +18,7 @@ + @@ -39,7 +41,7 @@ - + @@ -55,15 +57,23 @@ - + + + + + + + + + + - - - + + @@ -72,23 +82,25 @@ - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + @@ -118,7 +130,7 @@ - + @@ -251,8 +263,17 @@ - - + + + + + + + + + + + @@ -317,5 +338,6 @@ + diff --git a/AzureDevOpsBackup/AzureDevOpsBackup.csproj b/AzureDevOpsBackup/AzureDevOpsBackup.csproj index 17b2533..0d28333 100644 --- a/AzureDevOpsBackup/AzureDevOpsBackup.csproj +++ b/AzureDevOpsBackup/AzureDevOpsBackup.csproj @@ -156,6 +156,12 @@ + + + {050b25b3-9a27-44c2-a793-59357cfbcdc3} + AzureDevOpsBackupUnzipTool + + if $(ConfigurationName) == Release ( diff --git a/AzureDevOpsBackup/AzureDevOpsBackup.sln b/AzureDevOpsBackup/AzureDevOpsBackup.sln index d9298b8..49c92e0 100644 --- a/AzureDevOpsBackup/AzureDevOpsBackup.sln +++ b/AzureDevOpsBackup/AzureDevOpsBackup.sln @@ -15,6 +15,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Scripts\RenameOutputRelease.ps1 = Scripts\RenameOutputRelease.ps1 EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AzureDevOpsBackupUnzipTool", "..\AzureDevOpsBackupUnzipTool\AzureDevOpsBackupUnzipTool.csproj", "{050B25B3-9A27-44C2-A793-59357CFBCDC3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution All|Any CPU = All|Any CPU @@ -39,6 +41,14 @@ Global {24DA63B0-77D2-4861-8DC1-C47EB93E88EF}.DefaultBuild|Any CPU.Build.0 = DefaultBuild {24DA63B0-77D2-4861-8DC1-C47EB93E88EF}.Release|Any CPU.ActiveCfg = DefaultBuild {24DA63B0-77D2-4861-8DC1-C47EB93E88EF}.Release|Any CPU.Build.0 = DefaultBuild + {050B25B3-9A27-44C2-A793-59357CFBCDC3}.All|Any CPU.ActiveCfg = Debug|Any CPU + {050B25B3-9A27-44C2-A793-59357CFBCDC3}.All|Any CPU.Build.0 = Debug|Any CPU + {050B25B3-9A27-44C2-A793-59357CFBCDC3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {050B25B3-9A27-44C2-A793-59357CFBCDC3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {050B25B3-9A27-44C2-A793-59357CFBCDC3}.DefaultBuild|Any CPU.ActiveCfg = Debug|Any CPU + {050B25B3-9A27-44C2-A793-59357CFBCDC3}.DefaultBuild|Any CPU.Build.0 = Debug|Any CPU + {050B25B3-9A27-44C2-A793-59357CFBCDC3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {050B25B3-9A27-44C2-A793-59357CFBCDC3}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/AzureDevOpsBackup/Class/DisplayHelp.cs b/AzureDevOpsBackup/Class/DisplayHelp.cs index de037c6..08b024f 100644 --- a/AzureDevOpsBackup/Class/DisplayHelp.cs +++ b/AzureDevOpsBackup/Class/DisplayHelp.cs @@ -26,7 +26,8 @@ public static void DisplayGuide() Console.WriteLine("\t--server: IP address or DNS name of the SMTP server"); Console.WriteLine("\t--port: The port for the SMTP server"); Console.WriteLine("\t--from: The email address the report is send from"); - Console.WriteLine("\t--toemail: The email address the report is send to\n"); + Console.WriteLine("\t--to: The email address the report is send to - support multiple recipients with"); + Console.WriteLine("\t\t\t argument separated by comma"); Console.WriteLine(" Optional:"); Console.WriteLine("\t--tokenfile : Save a token to access the API in Azure DevOps to an encrypted token.bin file"); Console.WriteLine("\t\t\t (use this before using the '--token token.bin' argument!)"); @@ -36,6 +37,7 @@ public static void DisplayGuide() Console.WriteLine("\t\t\t be deleted (default is 30 dayes) (optional)"); Console.WriteLine("\t--simpelreport: If set the email report layout there is send is simple, if not set it use the default"); Console.WriteLine("\t\t\t report layout"); + Console.WriteLine("\t--noattatchlog: Set the email report to not attach the logfile in the mail report sent"); Console.WriteLine("\t--priority: Set the email report priority to other then default (normal)"); Console.WriteLine("\t high: Set the email report priority to 'high'"); Console.WriteLine("\t low: Set the email report priority to 'low'"); @@ -57,7 +59,9 @@ public static void DisplayGuide() Console.WriteLine($"\t{Globals._currentExeFileName} --token XXX... --org OrgName --backup C:\\Backup --server smtp.domain.local"); Console.WriteLine("\t--port 25 --from from@domain.local --to reports@domain.local --unzip --simpelreport --priority high\n"); Console.WriteLine($"\t{Globals._currentExeFileName} --token token.bin --org OrgName --backup C:\\Backup --server smtp.domain.local"); - Console.WriteLine("\t--port 25 --from from@domain.local --to reports@domain.local --unzip --simpelreport --priority low"); + Console.WriteLine("\t--port 25 --from from@domain.local --to reports@domain.local --unzip --simpelreport --priority low\n"); + Console.WriteLine($"\t{Globals._currentExeFileName} --token token.bin --org OrgName --backup C:\\Backup --server smtp.domain.local"); + Console.WriteLine("\t--port 25 --from from@domain.local --to reports@domain.local,admin@domain.local --unzip --noattatchlog"); Console.WriteLine(); Console.WriteLine("Output:"); Console.WriteLine("\tA timestamped folder containing the backup will be created within this directory unless --backup"); diff --git a/AzureDevOpsBackup/Class/Files.cs b/AzureDevOpsBackup/Class/Files.cs index 9559e36..1622e1b 100644 --- a/AzureDevOpsBackup/Class/Files.cs +++ b/AzureDevOpsBackup/Class/Files.cs @@ -13,7 +13,7 @@ public static string LogFilePath get { // Root folder for log files - var logfilePathvar = ProgramDataFilePath + @"\Log"; + var logfilePathvar = ProgramDataFilePath + @"\Log\Backups"; return logfilePathvar; } } diff --git a/AzureDevOpsBackup/Class/Globals.cs b/AzureDevOpsBackup/Class/Globals.cs index 84576b1..7966986 100644 --- a/AzureDevOpsBackup/Class/Globals.cs +++ b/AzureDevOpsBackup/Class/Globals.cs @@ -4,30 +4,30 @@ namespace AzureDevOpsBackup.Class { public static class Globals { - public static string _currentExeFileName; - public static int _errors; - public static string _vData; - public static string _companyName; - public static string _orgName; - public static string _startTime; - public static string _endTime; - public static int _totalFilesIsDeletedAfterUnZipped; - public static int _numZip; - public static int _numJson; - public static bool _checkForLeftoverFilesAfterCleanup; - public static bool _deletedFilesAfterUnzip; - public static string AppName; - public static string _copyrightData; - public static int _totalBackupsIsDeleted; - public static string _fileAttachedIneMailReport; - public static MailPriority EmailPriority = MailPriority.Normal; - public static int _currentBackupsInBackupFolderCount; + public static string _currentExeFileName; // The name of the current executable file + public static int _errors; // The number of errors that have occurred + public static string _vData; // The version data of the current executable file + public static string _companyName; // The company name of the current executable file + public static string _orgName; // The organization name of Azure DevOps to backup + public static string _startTime; // The time the backup started + public static string _endTime; // The time the backup ended + public static int _totalFilesIsDeletedAfterUnZipped; // The total number of files deleted after unzipping + public static int _numZip; // The number of zip files + public static int _numJson; // The number of json files + public static bool _checkForLeftoverFilesAfterCleanup; // Check for leftover files after cleanup + public static bool _deletedFilesAfterUnzip; // Delete files after unzipping + public static string AppName; // The name of the application + public static string _copyrightData; // The copyright data of the application + public static int _totalBackupsIsDeleted; // The total number of backups deleted + public static string _fileAttachedIneMailReport; // The file attached in the email report (name) + public static MailPriority EmailPriority = MailPriority.Normal; // The priority of the email report (default is normal) + public static int _currentBackupsInBackupFolderCount; public static int _oldLogFilesToDeleteCount; public static bool _oldLogfilesToDelete; - public const string APIversion = "api-version=7.0"; // https://learn.microsoft.com/en-us/rest/api/azure/devops/ - public static string _backupFolder; - public static string _sanitizedbackupFolder; - public static string _dateOfToday; + public const string APIversion = "api-version=7.0"; // See more at: https://learn.microsoft.com/en-us/rest/api/azure/devops/ for information on API versions + public static string _backupFolder; // The folder to backup to (default is the current directory) + public static string _sanitizedbackupFolder; // The sanitized folder to backup to (default is the current directory) + public static string _dateOfToday; // The date of today public static string _repoCountStatusText; public static string _totalFilesIsBackupUnZippedStatusText; public static string _totalBlobFilesIsBackupStatusText; @@ -39,12 +39,13 @@ public static class Globals public static string _isOutputFolderContainFilesStatusText; public static string _repoItemsCountStatusText; public static string _isDaysToKeepNotDefaultStatusText; - public static int _projectCount; - public static int _totalFilesIsBackupUnZipped; - public static int _totalBlobFilesIsBackup; - public static int _totalTreeFilesIsBackup; - public static int _repoItemsCount; - public static int _repoCount; - public static string _emailStatusMessage; + public static int _projectCount; // The number of projects in the organization to backup + public static int _totalFilesIsBackupUnZipped; // The total number of files in the backups unzipped + public static int _totalBlobFilesIsBackup; // The total number of blob files in the backups + public static int _totalTreeFilesIsBackup; // The total number of tree files in the backups + public static int _repoItemsCount; // The number of items in the repository + public static int _repoCount; // The number of repositories in the organization + public static string _emailStatusMessage; // The status message of the email + public static bool _noAttatchLog; // TODO } } \ No newline at end of file diff --git a/AzureDevOpsBackup/Class/ReportSender.cs b/AzureDevOpsBackup/Class/ReportSender.cs index d08e561..acf597b 100644 --- a/AzureDevOpsBackup/Class/ReportSender.cs +++ b/AzureDevOpsBackup/Class/ReportSender.cs @@ -17,7 +17,7 @@ public static void SendEmail(string serverAddress, string serverPort, string ema int totalFilesIsDeletedAfterUnZipped, int totalBackupsIsDeleted, string daysToKeep, string repoCountStatusText, string repoItemsCountStatusText, string totalFilesIsBackupUnZippedStatusText, string totalBlobFilesIsBackupStatusText, string totalTreeFilesIsBackupStatusText, string totalFilesIsDeletedAfterUnZippedStatusText, string letOverZipFilesStatusText, string letOverJsonFilesStatusText, string totalBackupsIsDeletedStatusText, - bool useSimpleMailReportLayout, string isOutputFolderContainFilesStatusText, string isDaysToKeepNotDefaultStatusText, string startTime, string endTime, bool deletedFilesAfterUnzip, + bool useSimpleMailReportLayout, bool noAttatchLog, string isOutputFolderContainFilesStatusText, string isDaysToKeepNotDefaultStatusText, string startTime, string endTime, bool deletedFilesAfterUnzip, bool checkForLeftoverFilesAfterCleanup) { var serverPortStr = serverPort; @@ -140,10 +140,22 @@ public static void SendEmail(string serverAddress, string serverPort, string ema } // Create mail - var message = new MailMessage(emailFrom, emailTo); + var message = new MailMessage(); + message.From = new MailAddress(emailFrom); + + // Split the emailTo string by commas and add each address to the To collection + var emailAddresses = emailTo.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + foreach (var address in emailAddresses) + { + message.To.Add(address.Trim()); + } + + // Set email subject message.Subject = "[" + emailStatusMessage + $"] - {Globals.AppName} status - (" + totalBlobFilesIsBackup + " Git projects backed up), " + errors + " issues(s) - (backups to keep (days): " + daysToKeep + ", backup(s) deleted: " + totalBackupsIsDeleted + ")"; + + // Set email body message.Body = mailBody; message.BodyEncoding = Encoding.UTF8; message.IsBodyHtml = true; @@ -162,88 +174,108 @@ public static void SendEmail(string serverAddress, string serverPort, string ema Console.WriteLine("Created email report and parsed data"); Console.ResetColor(); - // Get all the files in the log dir for today + // Check if we should attach the logfile to the email report or not + if (noAttatchLog) + { + // Log + Message("No logfile attached to email report!", EventType.Information, 1000); + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine("No logfile attached to email report!"); + Console.ResetColor(); + } + else + { + // Get all the files in the log dir for today - // Log - Message("Finding logfile for today to attach in email report...", EventType.Information, 1000); - Console.ForegroundColor = ConsoleColor.Yellow; - Console.WriteLine("Finding logfile for today to attach in email report..."); - Console.ResetColor(); + // Log + Message("Finding logfile for today to attach in email report...", EventType.Information, 1000); + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine("Finding logfile for today to attach in email report..."); + Console.ResetColor(); - // Get filename to find - var filePaths = Directory.GetFiles(Files.LogFilePath, $"{Globals.AppName} Log " + DateTime.Today.ToString("dd-MM-yyyy") + "*.*"); + // Get filename to find + var filePaths = Directory.GetFiles(Files.LogFilePath, + $"{Globals.AppName} Log " + DateTime.Today.ToString("dd-MM-yyyy") + "*.*"); - // Get the files that their extension are .log or .txt - var files = filePaths.Where(filePath => Path.GetExtension(filePath).Contains(".log") || Path.GetExtension(filePath).Contains(".txt")); + // Get the files that their extension are .log or .txt + var files = filePaths.Where(filePath => + Path.GetExtension(filePath).Contains(".log") || Path.GetExtension(filePath).Contains(".txt")); - // Loop through the files enumeration and attach each file in the mail. - foreach (var file in files) - { - Globals._fileAttachedIneMailReport = file; + // Loop through the files enumeration and attach each file in the mail. + foreach (var file in files) + { + Globals._fileAttachedIneMailReport = file; - // Log - Message("Found logfile for today:", EventType.Information, 1000); - Console.ForegroundColor = ConsoleColor.Green; - Console.WriteLine("Found logfile for today:"); - Console.ResetColor(); + // Log + Message("Found logfile for today:", EventType.Information, 1000); + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine("Found logfile for today:"); + Console.ResetColor(); - // Full file name - var fileName = Globals._fileAttachedIneMailReport; - var fi = new FileInfo(fileName); + // Full file name + var fileName = Globals._fileAttachedIneMailReport; + var fi = new FileInfo(fileName); - // Get File Name - var justFileName = fi.Name; - Console.WriteLine("File name: " + justFileName); - Message("File name: " + justFileName, EventType.Information, 1000); + // Get File Name + var justFileName = fi.Name; + Console.WriteLine("File name: " + justFileName); + Message("File name: " + justFileName, EventType.Information, 1000); - // Get file name with full path - var fullFileName = fi.FullName; - Console.WriteLine("Full file name: " + fullFileName); - Message("Full file name: " + fullFileName, EventType.Information, 1000); + // Get file name with full path + var fullFileName = fi.FullName; + Console.WriteLine("Full file name: " + fullFileName); + Message("Full file name: " + fullFileName, EventType.Information, 1000); - // Get file extension - var extn = fi.Extension; - Console.WriteLine("File Extension: " + extn); - Message("File Extension: " + extn, EventType.Information, 1000); + // Get file extension + var extn = fi.Extension; + Console.WriteLine("File Extension: " + extn); + Message("File Extension: " + extn, EventType.Information, 1000); - // Get directory name - var directoryName = fi.DirectoryName; - Console.WriteLine("Directory name: " + directoryName); - Message("Directory name: " + directoryName, EventType.Information, 1000); + // Get directory name + var directoryName = fi.DirectoryName; + Console.WriteLine("Directory name: " + directoryName); + Message("Directory name: " + directoryName, EventType.Information, 1000); - // File Exists ? - var exists = fi.Exists; - Console.WriteLine("File exists: " + exists); - Message("File exists: " + exists, EventType.Information, 1000); - if (fi.Exists) - { - // Get file size - var size = fi.Length; - Console.WriteLine("File Size in Bytes: " + size); - Message("File Size in Bytes: " + size, EventType.Information, 1000); + // File Exists ? + var exists = fi.Exists; + Console.WriteLine("File exists: " + exists); + Message("File exists: " + exists, EventType.Information, 1000); + if (fi.Exists) + { + // Get file size + var size = fi.Length; + Console.WriteLine("File Size in Bytes: " + size); + Message("File Size in Bytes: " + size, EventType.Information, 1000); - // File ReadOnly ? - var isReadOnly = fi.IsReadOnly; - Console.WriteLine("Is ReadOnly: " + isReadOnly); - Message("Is ReadOnly: " + isReadOnly, EventType.Information, 1000); + // File ReadOnly ? + var isReadOnly = fi.IsReadOnly; + Console.WriteLine("Is ReadOnly: " + isReadOnly); + Message("Is ReadOnly: " + isReadOnly, EventType.Information, 1000); - // Creation, last access, and last write time - var creationTime = fi.CreationTime; - Console.WriteLine("Creation time: " + creationTime); - Message("Creation time: " + creationTime, EventType.Information, 1000); - var accessTime = fi.LastAccessTime; - Console.WriteLine("Last access time: " + accessTime); - Message("Last access time: " + accessTime, EventType.Information, 1000); - var updatedTime = fi.LastWriteTime; - Console.WriteLine("Last write time: " + updatedTime + "\n"); - Message("Last write time: " + updatedTime, EventType.Information, 1000); - } + // Creation, last access, and last write time + var creationTime = fi.CreationTime; + Console.WriteLine("Creation time: " + creationTime); + Message("Creation time: " + creationTime, EventType.Information, 1000); + var accessTime = fi.LastAccessTime; + Console.WriteLine("Last access time: " + accessTime); + Message("Last access time: " + accessTime, EventType.Information, 1000); + var updatedTime = fi.LastWriteTime; + Console.WriteLine("Last write time: " + updatedTime + "\n"); + Message("Last write time: " + updatedTime, EventType.Information, 1000); + } + + // TODO Do not add more to logfile here - file is locked! + var attachment = new Attachment(file); - // TODO Do not add more to logfile here - file is locked! - var attachment = new Attachment(file); + // Attach file to email + message.Attachments.Add(attachment); + } - // Attach file to email - message.Attachments.Add(attachment); + // Log + Message("Logfile attached to email report!", EventType.Information, 1000); + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine("Logfile attached to email report!"); + Console.ResetColor(); } //Try to send email status email diff --git a/AzureDevOpsBackup/Program.cs b/AzureDevOpsBackup/Program.cs index 327a1d3..919b154 100644 --- a/AzureDevOpsBackup/Program.cs +++ b/AzureDevOpsBackup/Program.cs @@ -1667,6 +1667,8 @@ private static async Task Main(string[] args) // If args is set to old mail report layout var useSimpleMailReportLayout = Array.Exists(args, argument => argument == "--simpelreport"); + var noAttatchLog = Array.Exists(args, argument => argument == "--noattatchlog"); + // Send status email and parse data to function ReportSender.SendEmail(server, serverPort, emailFrom, emailTo, Globals._emailStatusMessage, repocountelements, repoitemscountelements, Globals._repoCount, Globals._repoItemsCount, Globals._totalFilesIsBackupUnZipped, Globals._totalBlobFilesIsBackup, @@ -1674,7 +1676,7 @@ private static async Task Main(string[] args) Globals._totalBackupsIsDeleted, daysToKeepBackups, Globals._repoCountStatusText, Globals._repoItemsCountStatusText, Globals._totalFilesIsBackupUnZippedStatusText, Globals._totalBlobFilesIsBackupStatusText, Globals._totalTreeFilesIsBackupStatusText, Globals._totalFilesIsDeletedAfterUnZippedStatusText, Globals._letOverZipFilesStatusText, Globals._letOverJsonFilesStatusText, - Globals._totalBackupsIsDeletedStatusText, useSimpleMailReportLayout, Globals._isOutputFolderContainFilesStatusText, + Globals._totalBackupsIsDeletedStatusText, useSimpleMailReportLayout, noAttatchLog, Globals._isOutputFolderContainFilesStatusText, Globals._isDaysToKeepNotDefaultStatusText, Globals._startTime, Globals._endTime, Globals._deletedFilesAfterUnzip, Globals._checkForLeftoverFilesAfterCleanup); #endregion Status mail data collecting diff --git a/AzureDevOpsBackup/Properties/AssemblyInfo.cs b/AzureDevOpsBackup/Properties/AssemblyInfo.cs index 2f20688..cb4536f 100644 --- a/AzureDevOpsBackup/Properties/AssemblyInfo.cs +++ b/AzureDevOpsBackup/Properties/AssemblyInfo.cs @@ -31,5 +31,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.6.0")] -[assembly: AssemblyFileVersion("1.0.6.0")] +[assembly: AssemblyVersion("1.1.0.0")] +[assembly: AssemblyFileVersion("1.1.0.0")] diff --git a/AzureDevOpsBackupUnzipTool/App.config b/AzureDevOpsBackupUnzipTool/App.config new file mode 100644 index 0000000..193aecc --- /dev/null +++ b/AzureDevOpsBackupUnzipTool/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/AzureDevOpsBackupUnzipTool/Azure Dev Ops.ico b/AzureDevOpsBackupUnzipTool/Azure Dev Ops.ico new file mode 100644 index 0000000..e5509b1 Binary files /dev/null and b/AzureDevOpsBackupUnzipTool/Azure Dev Ops.ico differ diff --git a/AzureDevOpsBackupUnzipTool/AzureDevOpsBackupUnzipTool.csproj b/AzureDevOpsBackupUnzipTool/AzureDevOpsBackupUnzipTool.csproj new file mode 100644 index 0000000..4badd42 --- /dev/null +++ b/AzureDevOpsBackupUnzipTool/AzureDevOpsBackupUnzipTool.csproj @@ -0,0 +1,87 @@ + + + + + Debug + AnyCPU + {050B25B3-9A27-44C2-A793-59357CFBCDC3} + Exe + AzureDevOpsBackupUnzipTool + AzureDevOpsBackupUnzipTool + v4.8 + 512 + true + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + none + true + bin\Release\ + TRACE + prompt + 4 + + + Azure Dev Ops.ico + + + + ..\AzureDevOpsBackup\packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll + + + + + + + ..\AzureDevOpsBackup\packages\System.IO.Compression.ZipFile.4.3.0\lib\net46\System.IO.Compression.ZipFile.dll + True + True + + + + + + + + + + + + + + + + + + + + + + + + + + + + + if $(ConfigurationName) == Release ( + echo "Release build - signing output file(s)!". + "C:\Program Files (x86)\Windows Kits\10\App Certification Kit\signtool.exe" sign /n "Michael Morten Sonne" /fd sha256 /tr http://timestamp.sectigo.com /td sha256 /as /v "$(TargetPath)" + echo "Release build - signing output file(s) done!". +) ELSE ( + echo "Debug build - no tasks!". +) + + + \ No newline at end of file diff --git a/AzureDevOpsBackupUnzipTool/Class/ApplicationInfo.cs b/AzureDevOpsBackupUnzipTool/Class/ApplicationInfo.cs new file mode 100644 index 0000000..6d8958c --- /dev/null +++ b/AzureDevOpsBackupUnzipTool/Class/ApplicationInfo.cs @@ -0,0 +1,37 @@ +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; + +namespace AzureDevOpsBackupUnzipTool.Class +{ + internal class ApplicationInfo + { + public static void GetExeInfo() + { + // Get application data to later use in tool and log + AssemblyCopyrightAttribute copyright = Assembly.GetExecutingAssembly().GetCustomAttributes(typeof(AssemblyCopyrightAttribute), false)[0] as AssemblyCopyrightAttribute; + // ReSharper disable once PossibleNullReferenceException + Globals._copyrightData = copyright.Copyright; + + // Get application data to later use in tool and log + Globals._vData = Assembly.GetEntryAssembly()?.GetName().Version.ToString(); + var attributes = typeof(Program).GetTypeInfo().Assembly.GetCustomAttributes(typeof(AssemblyTitleAttribute)); + var assemblyTitleAttribute = attributes.SingleOrDefault() as AssemblyTitleAttribute; + + // Set application name in code and log + Globals.AppName = assemblyTitleAttribute?.Title; + + // Set exe file name in code and log + Globals._currentExeFileName = Path.GetFileName(Process.GetCurrentProcess().MainModule?.FileName); + + // Set company name in code and log + var fileName = Assembly.GetEntryAssembly()?.Location; + if (fileName != null) + { + var versionInfo = FileVersionInfo.GetVersionInfo(fileName); + Globals._companyName = versionInfo.CompanyName; + } + } + } +} diff --git a/AzureDevOpsBackupUnzipTool/Class/DisplayHelp.cs b/AzureDevOpsBackupUnzipTool/Class/DisplayHelp.cs new file mode 100644 index 0000000..3727c4a --- /dev/null +++ b/AzureDevOpsBackupUnzipTool/Class/DisplayHelp.cs @@ -0,0 +1,63 @@ +using System; + +namespace AzureDevOpsBackupUnzipTool.Class +{ + internal class DisplayHelpToConsole + { + public static void DisplayGuide() + { + Console.WriteLine("Usage:"); + Console.WriteLine($"\t{Globals._currentExeFileName} --zipFile --jsonFile --output "); + Console.WriteLine(); + Console.WriteLine("Description:"); + Console.WriteLine("\tAzure DevOps Backup unzip tool for Git Projects (single repos to unzip from metadata) from the Azure DevOps API."); + Console.WriteLine("\t(Part of Azure DevOps Backup tool from Michael Morten Sonne)"); + Console.WriteLine(); + Console.WriteLine("\tWhile the code is perfectly safe on the Azure infrastructure, there are cases where a centralized"); + Console.WriteLine("\tlocal backup of all projects and repositories is needed. These might include Corporate Policies,"); + Console.WriteLine("\tDisaster Recovery and Business Continuity Plans."); + Console.WriteLine(); + Console.WriteLine("Parameter List:"); + Console.WriteLine(" Mandatory:"); + Console.WriteLine("\t--zipFile: Name of the .zip folder to rename GUID´s to file or folders"); + Console.WriteLine("\t--jsonFile: Name of the .json file with the metadata in to rename GUID´s to files and folders"); + Console.WriteLine("\t--output: Folder to unzip data into"); + Console.WriteLine(); + Console.WriteLine(" Optional:"); + Console.WriteLine("\t--help, /h or /?: Showing this help text for the tool"); + Console.WriteLine("\t--info or /about: Showing information about the tool"); + Console.WriteLine(); + Console.WriteLine("Examples:"); + Console.WriteLine($"\t{Globals._currentExeFileName} --zipFile \"C:\\Temp\\blob.zip\" --jsonFile \"C:\\Temp\\tree.json\" --output \"C:\\Temp\\Unzipped\""); + Console.WriteLine(); + Console.WriteLine("Output:"); + Console.WriteLine("\tData from Git repo is renamed to the right filenames or folders based on the metadata from the Azure DevOps API."); + Console.WriteLine("\tMapping the files together with the structure of the repo."); + Console.WriteLine(); + } + + public static void DisplayInfo() + { + Console.WriteLine("Description:"); + Console.WriteLine("\tAzure DevOps Backup unzip tool for Git Projects (single repos to unzip from metadata) from the Azure DevOps API."); + Console.WriteLine(); + Console.WriteLine("\tWhile the code is perfectly safe on the Azure infrastructure, there are cases where a centralized"); + Console.WriteLine("\tlocal backup of all projects and repositories is needed. These might include Corporate Policies,"); + Console.WriteLine("\tDisaster Recovery and Business Continuity Plans."); + Console.WriteLine(); + Console.WriteLine("\tAzure DevOps is a cloud service to manage source code and collaborate between development teams."); + Console.WriteLine("\tIt integrates perfectly with both Visual Studio and Visual Studio Code and other IDE´s and tools"); + Console.WriteLine("\tthere is using the 'Git'."); + Console.WriteLine(); + Console.WriteLine("My blog:"); + Console.WriteLine("\thttps://blog.sonnes.cloud"); + Console.WriteLine(); + Console.WriteLine("My Website:"); + Console.WriteLine("\thttps://sonnes.cloud"); + Console.WriteLine(); + Console.WriteLine("See Microsoft´s website for more information about Azure DevOps:"); + Console.WriteLine("\thttps://azure.microsoft.com/en-us/products/devops"); + Console.WriteLine(); + } + } +} \ No newline at end of file diff --git a/AzureDevOpsBackupUnzipTool/Class/FileLogger.cs b/AzureDevOpsBackupUnzipTool/Class/FileLogger.cs new file mode 100644 index 0000000..5ff87d0 --- /dev/null +++ b/AzureDevOpsBackupUnzipTool/Class/FileLogger.cs @@ -0,0 +1,150 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Security; + +namespace AzureDevOpsBackupUnzipTool.Class +{ + internal class FileLogger + { + // Control if saves log to logfile + public static bool WriteToFile { get; set; } = true; + + // Control if saves log to Windows eventlog + public static bool WriteToEventLog { get; set; } = true; + + public static bool WriteOnlyErrorsToEventLog { get; set; } = true; + + // Sets the App name for the log function + //public static string AppName { get; set; } = Globals.AppName; // "Unknown",; + + // Set date format short + public static string DateFormat { get; set; } = "dd-MM-yyyy"; + + // Set date format long + public static string DateTimeFormat { get; set; } = "dd-MM-yyyy HH:mm:ss"; + + // Get logfile path + public static string GetLogPath(string df) + { + return Files.LogFilePath + @"\" + Globals.AppName + " Log " + df + ".log"; + } + + // Get datetime + public static string GetDateTime(DateTime datetime) + { + return datetime.ToString(DateTimeFormat); + } + + // Get date + public static string GetDate(DateTime datetime) + { + return datetime.ToString(DateFormat); + } + + // Set event type + public enum EventType + { + Warning, + Error, + Information, + } + + // Add message + public static void Message(string logText, EventType type, int id) + { + var now = DateTime.Now; + var date = GetDate(now); + var dateTime = GetDateTime(now); + var logPath = GetLogPath(date); + + // Set where to save log message to + if (WriteToFile) + AppendMessageToFile(logText, type, dateTime, logPath, id); + if (!WriteToEventLog) + return; + AddMessageToEventLog(logText, type, dateTime, logPath, id); + } + + // Save message to logfile + private static void AppendMessageToFile(string mess, EventType type, string dtf, string path, int id) + { + try + { + // Check if file exists else create it + if (!Directory.Exists(Files.LogFilePath)) + Directory.CreateDirectory(Files.LogFilePath); + + var str = type.ToString().Length > 7 ? "\t" : "\t\t"; + if (!File.Exists(path)) + { + using (var text = File.CreateText(path)) + text.WriteLine( + $"{(object)dtf} - [EventID {(object)id.ToString()}] {(object)type.ToString()}{(object)str}{(object)mess}"); + } + else + { + using (var streamWriter = File.AppendText(path)) + streamWriter.WriteLine( + $"{(object)dtf} - [EventID {(object)id.ToString()}] {(object)type.ToString()}{(object)str}{(object)mess}"); + } + } + catch (Exception ex) + { + if (!WriteToEventLog) + return; + AddMessageToEventLog($"Error writing to log file, {ex.Message}", EventType.Error, dtf, path, 0); + AddMessageToEventLog("Writing log file have been disabled.", EventType.Information, dtf, path, 0); + WriteToFile = false; + } + } + + // Save message to Windows event log + private static void AddMessageToEventLog(string mess, EventType type, string dtf, string path, int id) + { + try + { + if (type != EventType.Error && WriteOnlyErrorsToEventLog) + return; + var eventLog = new EventLog(""); + if (!EventLog.SourceExists(Globals.AppName)) + EventLog.CreateEventSource(Globals.AppName, "Application"); + eventLog.Source = Globals.AppName; + eventLog.EnableRaisingEvents = true; + var type1 = EventLogEntryType.Error; + switch (type) + { + case EventType.Warning: + type1 = EventLogEntryType.Warning; + break; + case EventType.Error: + type1 = EventLogEntryType.Error; + break; + case EventType.Information: + type1 = EventLogEntryType.Information; + break; + } + eventLog.WriteEntry(mess, type1, id); + } + catch (SecurityException ex) + { + if (WriteToFile) + { + AppendMessageToFile($"Security exception: {ex.Message}", EventType.Error, dtf, path, id); + AppendMessageToFile("Run this software as Administrator once to solve the problem.", EventType.Information, dtf, path, id); + AppendMessageToFile("Event log entries have been disabled.", EventType.Information, dtf, path, id); + WriteToEventLog = false; + } + } + catch (Exception ex) + { + if (WriteToFile) + { + AppendMessageToFile(ex.Message, EventType.Error, dtf, path, id); + AppendMessageToFile("Event log entries have been disabled.", EventType.Information, dtf, path, id); + WriteToEventLog = false; + } + } + } + } +} \ No newline at end of file diff --git a/AzureDevOpsBackupUnzipTool/Class/Files.cs b/AzureDevOpsBackupUnzipTool/Class/Files.cs new file mode 100644 index 0000000..a741279 --- /dev/null +++ b/AzureDevOpsBackupUnzipTool/Class/Files.cs @@ -0,0 +1,28 @@ +using System.IO; + +namespace AzureDevOpsBackupUnzipTool.Class +{ + internal class Files + { + public static string LogFilePath + { + get + { + // Root folder for log files + var logfilePathvar = ProgramDataFilePath + @"\Log\Unzip tool"; + return logfilePathvar; + } + } + + public static string ProgramDataFilePath + { + get + { + // Root path for program data + var currentDirectory = Directory.GetCurrentDirectory(); + var programDataFilePathvar = currentDirectory; + return programDataFilePathvar; + } + } + } +} \ No newline at end of file diff --git a/AzureDevOpsBackupUnzipTool/Class/Globals.cs b/AzureDevOpsBackupUnzipTool/Class/Globals.cs new file mode 100644 index 0000000..00f0b42 --- /dev/null +++ b/AzureDevOpsBackupUnzipTool/Class/Globals.cs @@ -0,0 +1,13 @@ +namespace AzureDevOpsBackupUnzipTool.Class +{ + public static class Globals + { + public static string _currentExeFileName; + public static string _companyName; + public static string _copyrightData; + public static string _vData; + public static string AppName; + public static bool _oldLogfilesToDelete; + public static int _oldLogFilesToDeleteCount; + } +} \ No newline at end of file diff --git a/AzureDevOpsBackupUnzipTool/Class/LocalFolderTasks.cs b/AzureDevOpsBackupUnzipTool/Class/LocalFolderTasks.cs new file mode 100644 index 0000000..5a50d8d --- /dev/null +++ b/AzureDevOpsBackupUnzipTool/Class/LocalFolderTasks.cs @@ -0,0 +1,72 @@ +using System; +using System.IO; +using System.Linq; +using static AzureDevOpsBackupUnzipTool.Class.FileLogger; + +namespace AzureDevOpsBackupUnzipTool.Class +{ + internal class LocalFolderTasks + { + public static void DeleteDirectory(string path) + { + foreach (string directory in Directory.GetDirectories(path)) + { + DeleteDirectory(directory); + } + try + { + Directory.Delete(path, true); + } + catch (IOException) + { + Directory.Delete(path, true); + } + catch (UnauthorizedAccessException) + { + Directory.Delete(path, true); + } + } + + public static void CreateLogFolder() + { + // If logfile path not exist, create it + if (!Directory.Exists(Files.LogFilePath)) + { + try + { + // Create log folder if not exist + Directory.CreateDirectory(Files.LogFilePath); + + // Log + Message("Log folder is created: '" + Files.LogFilePath + "'", EventType.Information, 1000); + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine("Log folder is created: '" + Files.LogFilePath + "'"); + Console.ResetColor(); + } + catch (UnauthorizedAccessException) + { + // Log + Message("! Unable to create folder to store the logs: '" + Files.LogFilePath + "'. Make sure the account you use to run this tool has write rights to this location.", EventType.Error, 1001); + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine("Unable to create folder to store the logs: '" + Files.LogFilePath + "'. Make sure the account you use to run this tool has write rights to this location."); + Console.ResetColor(); + } + catch (Exception e) + { + // Error when create logs folder + Message("Exception caught when trying to create logs folder - error: " + e, EventType.Error, 1001); + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine("{0} Exception caught.", e); + Console.ResetColor(); + } + } + } + + // Function to sanitize directory name + public static string SanitizeDirectoryName(string directoryName) + { + // Remove any potentially dangerous characters from the directory name + return Path.GetInvalidPathChars().Aggregate(directoryName, (current, c) => current.Replace(c.ToString(), string.Empty)); + } + } +} \ No newline at end of file diff --git a/AzureDevOpsBackupUnzipTool/Class/LocalLogCleanup.cs b/AzureDevOpsBackupUnzipTool/Class/LocalLogCleanup.cs new file mode 100644 index 0000000..c5a237e --- /dev/null +++ b/AzureDevOpsBackupUnzipTool/Class/LocalLogCleanup.cs @@ -0,0 +1,80 @@ +using System; +using System.IO; +using static AzureDevOpsBackupUnzipTool.Class.FileLogger; + +namespace AzureDevOpsBackupUnzipTool.Class +{ + internal class LocalLogCleanup + { + public static void CleanupLogs() + { + // Cleanup old log files + string[] oldfiles = Directory.GetFiles(Files.LogFilePath); + + // Log + Message("Checking for old log file(s) to cleanup...", EventType.Information, 1000); + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine("Checking for old log file(s) to cleanup..."); + Console.ResetColor(); + + // Loop all files in folder + foreach (string file in oldfiles) + { + FileInfo fi = new FileInfo(file); + + // Get all last access time back in time + if (fi.LastAccessTime < DateTime.Now.AddDays(-30)) + { + try + { + // Do work + fi.Delete(); + + // Log + Message("> Deleted old log file: '" + fi + "'", EventType.Information, 1000); + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine("Deleted old log file: '" + fi + "'"); + Console.ResetColor(); + + // Set status + Globals._oldLogfilesToDelete = true; + Globals._oldLogFilesToDeleteCount++; + } + catch (UnauthorizedAccessException) + { + Message("! Unable to delete old log file: '" + fi + "'. Make sure the account you use to run this tool has delete rights to this location.", EventType.Error, 1001); + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine("Unable to delete old log file: '" + fi + "'. Make sure the account you use to run this tool has delete rights to this location."); + Console.ResetColor(); + } + catch (Exception ex) + { + // Log + Message("Sorry, we are unable to delete old log file: '" + fi + "' - Error: " + ex, EventType.Error, 1001); + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine("Sorry, we are unable to delete old log file: '" + fi + "' - Error: " + ex); + Console.ResetColor(); + } + } + } + + // Check if there is old log files to delete + if (Globals._oldLogfilesToDelete) + { + // Log + Message($"There was '{Globals._oldLogFilesToDeleteCount}' old log files to delete (-30 days)", EventType.Information, 1000); + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine($"There was '{Globals._oldLogFilesToDeleteCount}' old log files to delete (-30 days)"); + Console.ResetColor(); + } + else + { + // Log + Message("No old log files to delete (-30 days)", EventType.Information, 1000); + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine("No old log files to delete (-30 days)"); + Console.ResetColor(); + } + } + } +} \ No newline at end of file diff --git a/AzureDevOpsBackupUnzipTool/Program.cs b/AzureDevOpsBackupUnzipTool/Program.cs new file mode 100644 index 0000000..6d6c3d6 --- /dev/null +++ b/AzureDevOpsBackupUnzipTool/Program.cs @@ -0,0 +1,243 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using AzureDevOpsBackupUnzipTool.Class; +using Newtonsoft.Json; +using static AzureDevOpsBackupUnzipTool.Class.FileLogger; + +namespace AzureDevOpsBackupUnzipTool +{ + public class RootObject + { + public int Count { get; set; } + public List Value { get; set; } + } + + public class Item + { + public string ObjectId { get; set; } + public string GitObjectType { get; set; } + public string CommitId { get; set; } + public string Path { get; set; } + public string Url { get; set; } + } + + class Program + { + static void Main(string[] args) + { + // Get application information + ApplicationInfo.GetExeInfo(); + + // Create log folder if not exist + LocalFolderTasks.CreateLogFolder(); + + // Check if parameters have been provided and contains one of + if (args.Length == 0 || args.Contains("--help") || args.Contains("/h") || args.Contains("/?") || args.Contains("/info") || args.Contains("/about")) + { + // If none arguments + if (args.Length == 0) + { + // No arguments have been provided + Message("ERROR: No arguments is provided - try again!", EventType.Error, 1001); + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine("ERROR: No arguments is provided - try again!\n"); + Console.ResetColor(); + + // Show help to console + DisplayHelpToConsole.DisplayGuide(); + + // Log + Message($"Showed help to Console - Exciting {Globals.AppName}, v." + Globals._vData + " by " + Globals._companyName + "!", EventType.Information, 1000); + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine($"Showed help to Console - Exciting {Globals.AppName}, v." + Globals._vData + " by " + Globals._companyName + "!\n"); + Console.ResetColor(); + + // End application + Environment.Exit(1); + } + + // If wants help + if (args.Contains("--help") || args.Contains("/h") || args.Contains("/?")) + { + // Show help to console + DisplayHelpToConsole.DisplayGuide(); + + // Log + Message($"Showed help to Console - Exciting {Globals.AppName}, v." + Globals._vData + " by " + Globals._companyName + "!", EventType.Information, 1000); + + // Reset color + Console.ResetColor(); + + // End application + Environment.Exit(1); + } + + // If wants information about application + if (args.Contains("/info") || args.Contains("/about")) + { + // Show information about application to console + DisplayHelpToConsole.DisplayInfo(); + + // Log + Message($"Showed information about application to Console - Exciting {Globals.AppName}, v." + Globals._vData + " by " + Globals._companyName + "!", EventType.Information, 1000); + + // Reset color + Console.ResetColor(); + + // End application + Environment.Exit(1); + } + } + + // Cleanup old log files + LocalLogCleanup.CleanupLogs(); + + // Check for required Args for application will work + string[] requiredArgs = { "--zipFile", "--jsonFile", "--output" }; + + foreach (var requiredArg in requiredArgs) + { + if (!args.Contains(requiredArg)) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"ERROR: Missing required argument '{requiredArg}'"); + Console.ResetColor(); + return; + } + } + + // Check if the required arguments are provided + if (args.Length < 3) + { + // Show help to console + DisplayHelpToConsole.DisplayGuide(); + return; + } + + // Get the required arguments from the command line arguments and save them to variables + string zipFilePath = args[Array.IndexOf(args, "--zipFile") + 1]; + string jsonFilePath = args[Array.IndexOf(args, "--jsonFile") + 1]; + string outputDirectory = args[Array.IndexOf(args, "--output") + 1]; + + //Try to unzip the project form the zip file and metadata file + try + { + // Do + UnzipProject(zipFilePath, outputDirectory, jsonFilePath); + + // Log + Message("Unzipping completed successfully!", EventType.Information, 1000); + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine("\n> Unzipping completed successfully!\n"); + Console.ResetColor(); + } + catch (Exception ex) + { + // Log + Message($"An error occurred: {ex.Message}", EventType.Error, 1001); + Console.WriteLine($"An error occurred: {ex.Message}"); + } + } + + private static void UnzipProject(string zipFilePath, string outputDirectory, string jsonFilePath) + { + // Read JSON data + string jsonData = File.ReadAllText(jsonFilePath); + var rootObject = JsonConvert.DeserializeObject(jsonData); + var items = rootObject.Value; + + // Open the zip archive + using (var archive = ZipFile.OpenRead(zipFilePath)) + { + foreach (var item in items) + { + // Get the destination path + string destinationPath = Path.GetFullPath(Path.Combine(outputDirectory, item.Path.TrimStart('/'))); + + // Check if the item is a folder or a file + if (item.GitObjectType == "tree") + { + // If folder data + Console.WriteLine($"Unzipping Git repository folder data: '{destinationPath}'..."); + + try + { + // Create backup folder if not exist + Directory.CreateDirectory(destinationPath); + + // Log + Message("Output folder is created: '" + destinationPath + "'", EventType.Information, 1000); + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine("Output folder is created: '" + destinationPath + "'"); + Console.ResetColor(); + } + catch (UnauthorizedAccessException) + { + // Log + Message("! Unable to create folder to store the backups: '" + destinationPath + "'. Make sure the account you use to run this tool has write rights to this location.", EventType.Error, 1001); + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine("Unable to create folder to store the backups: '" + destinationPath + "'. Make sure the account you use to run this tool has write rights to this location."); + Console.ResetColor(); + } + catch (Exception e) + { + // Error when create backup folder + Message("Exception caught when trying to create output folder - error: " + e, EventType.Error, 1001); + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine("{0} Exception caught.", e); + Console.ResetColor(); + } + } + else if (item.GitObjectType == "blob") + { + // If file data + Console.WriteLine($"Unzipping Git repository file data on disk: '{destinationPath}'..."); + Message($"Unzipping Git repository file data on disk: '{destinationPath}'...", EventType.Information, 1000); + + // Extract the file + var entry = archive.GetEntry(item.ObjectId); + + // Check if the entry is not null + if (entry != null) + { + try + { + entry.ExtractToFile(destinationPath, true); + + // Log + Message($"Unzipped Git repository file data on disk: '{destinationPath}'", EventType.Information, 1000); + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine($"Unzipped Git repository file data on disk: '{destinationPath}'"); + Console.ResetColor(); + } + catch (UnauthorizedAccessException) + { + // Log + Message("! Unable to create folder to store the backups: '" + destinationPath + "'. Make sure the account you use to run this tool has write rights to this location.", EventType.Error, 1001); + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine("Unable to create folder to store the backups: '" + destinationPath + "'. Make sure the account you use to run this tool has write rights to this location."); + Console.ResetColor(); + } + catch (Exception e) + { + // Error when create backup folder + Message("Exception caught when trying to create output folder - error: " + e, EventType.Error, 1001); + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine("{0} Exception caught.", e); + Console.ResetColor(); + } + } + // If the entry is null + else + { + Console.WriteLine($"Entry with ObjectId '{item.ObjectId}' not found in the zip archive."); + } + } + } + } + } + } +} \ No newline at end of file diff --git a/AzureDevOpsBackupUnzipTool/Properties/AssemblyInfo.cs b/AzureDevOpsBackupUnzipTool/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..2ab96a8 --- /dev/null +++ b/AzureDevOpsBackupUnzipTool/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("AzureDevOpsBackupUnzipTool")] +[assembly: AssemblyDescription("Azure DevOps Backup unzip tool - Extract downloaded .zip Git projects from API for Azure DevOps for a single repo")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Michael Morten Sonne")] +[assembly: AssemblyProduct("AzureDevOpsBackupUnzipTool")] +[assembly: AssemblyCopyright("Copyright © Michael Morten Sonne")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("050b25b3-9a27-44c2-a793-59357cfbcdc3")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/AzureDevOpsBackupUnzipTool/Resources/Azure Dev Ops.ico b/AzureDevOpsBackupUnzipTool/Resources/Azure Dev Ops.ico new file mode 100644 index 0000000..e5509b1 Binary files /dev/null and b/AzureDevOpsBackupUnzipTool/Resources/Azure Dev Ops.ico differ diff --git a/AzureDevOpsBackupUnzipTool/packages.config b/AzureDevOpsBackupUnzipTool/packages.config new file mode 100644 index 0000000..1fb72a0 --- /dev/null +++ b/AzureDevOpsBackupUnzipTool/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 0750086..71ac970 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,21 @@ +## [1.1.0.0] - 10-08-2024 + +Major update with new features and bug fixes + +### Added +- Added a new tool (**AzureDevOpsBackupUnzipTool**) to the application, there let you unzip backups from .zip files based on the backup folder with the metadata files (.json), so the backups can be restored for a single project to save disk space vs unzipping all based on how many projects you have to backup, if you only need to restore a single project and not want to unzip the whole backup for all projects: + - Sample command: `.\AzureDevOpsBackupUnzipTool.exe --zipFile "C:\Temp\Test\master_blob.zip" --output "C:\Temp\Test\Test" --jsonFile "C:\Temp\Test\tree.json"` +- An option to not attach the logfile to the email report with argument: '**--noattatchlog**' +- Added support to send email report to multiple recipients with argument: '**--to**' - separated by comma + +### Changed +- Changed default install folder name (reflects only the installer) +- Changed logfile location in the **'.\Log'** folder - now in a subfolder the the 2 tools to it not being mixed and supports for cleanup: + - **AzureDevOpsBackupTool.exe**: **'.\Logs\Backup'** + - **AzureDevOpsBackupUnzipTool.exe**: **'.\Logs\Unzip tool'** + +### Fixed +- A lot documentation and help text overall fixed/added ## [1.0.6.0] - 08-08-2024 diff --git a/README.md b/README.md index d70fc3f..9a61ceb 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,8 @@

- +
+ Buy Me A Coffee

@@ -42,24 +43,28 @@ We can download a repository from Azure DevOps as a Zip file, but this may not b Outline the file contents of the repository. It helps users navigate the codebase, build configuration and any related assets. -| File/folder | Description | -|--------------------|---------------------------------------------| -| `AzureDevOpsBackup`| Source code. | -| `docs` | Documents/pictures. | -| `.gitignore` | Define what to ignore at commit time. | -| `CHANGELOG.md` | List of changes to the sample. | -| `CONTRIBUTING.md` | Guidelines for contributing to the AzureDevOpsBackupTool.| -| `README.md` | This README file. | -| `SECURITY.md` | Security file. | -| `LICENSE` | The license for the AzureDevOpsBackupTool. | +| File/folder | Description | +|------------------------------|---------------------------------------------| +| `AdvancedInstaller` | Code for installer project | +| `AzureDevOpsBackup` | Source code for the main backup tool itself.| +| `AzureDevOpsBackupUnzipTool` | Source code for the unzip tool itself. | +| `docs` | Documents/pictures. | +| `.gitignore` | Define what to ignore at commit time. | +| `CHANGELOG.md` | List of changes to the sample. | +| `CONTRIBUTING.md` | Guidelines for contributing to the AzureDevOpsBackupTool.| +| `README.md` | This README file. | +| `SECURITY.md` | Security file. | +| `LICENSE` | The license for the AzureDevOpsBackupTool. | ## Features -### Overall: +### AzureDevOpsBackup: + +#### Overall: - Asynchronous Resolution: Utilizes asynchronous processing for improved performance and responsiveness, allowing users to continue working while backups are being created. - Simplicity and Ease of Use: Provides a straightforward and user-friendly method for creating backups from Azure DevOps repositories. -### List: +#### List: - Backup Functionality: - Repository backup: Enables users to create local backups of Azure DevOps repositories. - Customizable backup Options: Offers various command-line options to tailor the backup process, including specifying backup directories, token authentication, cleanup, backup retention period, and more. @@ -75,6 +80,12 @@ Outline the file contents of the repository. It helps users navigate the codebas - Logging: - Job logging: Stores logs for backup jobs in a designated folder (.\Log) for a defined period (default: 30 days) beside the **AzureDevOpsBackup.exe** executable. +### AzureDevOpsBackup unzip tool: + +#### Overall: +- Unzip functionality: allows users to extract ZIP files and JSON metadata into a directory structure, renaming GUIDs to file or folder names for a specific repository backup. +- Simplicity and ease of use: Provides a straightforward and user-friendly method for unzipping repository backups from Azure DevOps .zip files. + ## Download [Download the latest version](../../releases/latest) @@ -116,9 +127,11 @@ Note we are also saving the original JSON item list we got from the repository c ## Usage -Paramenters: +### AzureDevOpsBackup: -Backup: +**Paramenters:** + +**Backup**: - --token - token.bin: Use an encrypted .bin file (based on hardware ID´s) with your personal access token in. (Remember to run --tokenfile to create the file first beside the application .exe!) @@ -134,14 +147,16 @@ Backup: - --daystokeepbackup: Set the number of days to retain backup files before automatic removal (default is 30 days if not specified). - --simpelreportlayout: Use this option to use the old email report layout. - --priority : Specify the email priority for notifications (e.g., high, normal, low). Default (normal) if not set. +- --noattatchlog: Use this option to not attatch the log file to the email report. -General: +**General**: - --help, /h or /?: Show the help menu - --info or /about: Show the about menu +#### Mandatory arguments: Mandatory arguments is: **`--token, --org, --outdir, --server, --port, --from and --to`** -**A bit more information about some arguments:** +#### **A bit more information about some arguments:** If the **--unzip** argument is present, the program will create a directory for each repository based on the information provided by each Zip/JSON file pair. In this directory, we will get the original file and folder structure with real file names and extensions. Looping through all the items on the JSON list file, we consider a simple condition: if the item is a folder we create the directory according to the item.path property. Otherwise, we assume it’s a blob and we extract it from the Zip archive into the corresponding directory assigning the original file name and extension. @@ -153,6 +168,42 @@ It looks at the backup folder, and see when last changed. If the days matches th If the **--simpelreportlayout** argument is present, the program will use the old email report layout, else it will use the new default. +### AzureDevOpsBackup unzip tool: + +**Paramenters:** + +Unzip from metadata: +- --zipFile: Name of the .zip folder to rename GUID´s to file or folders +- --jsonFile: Name of the .json file with the metadata in to rename GUID´s to files and folders +- --output: Folder to unzip data into + +General: +- --help, /h or /?: Show the help menu +- --info or /about: Show the about menu + +## Task scheduler (Windows) + +The AzureDevOpsBackup.exe tool can be run via Task Scheduler in Windows. This way, you can schedule the backup to run at specific intervals, ensuring that your Azure DevOps repositories are backed up regularly. + +The setup for that to work (via my testing and useing over long time) is to create a new task in Task Scheduler with the following settings: + +- **General:** + - **Name**: Azure DevOps Backup or similar + - **Description**: Backup of Azure DevOps repositories or similar + - **User account**: Select a user account with the necessary permissions to run the backup job and to wite to the application path (for logs) + - **Security options**: Run whether user is logged on or not + - **Security options**: Run with highest privileges + - **Trigger**: Set the desired schedule for the backup job (e.g., daily, weekly, etc.) + - **Action**: Start a program + - **Program/script**: Path to the AzureDevOpsBackup.exe executable + - **Add arguments**: Add the necessary command-line arguments for the backup job + - **Start in**: Path to the folder containing the AzureDevOpsBackup.exe executable + + **Sample**: + - **Program/script**: `D:\AzureDevOpsBackup\AzureDevOpsBackup.exe` + - **Add arguments**: `--token "xxxxxx" --org "YourOrg" --backup "D:\Backup\Azure DevOps" --server "domain-com.mail.protection.outlook.com" --port "25" --from "azure-devops-backup@domain.com" --to "AZ-DL-AzureDevOpsBackupReports@domain.com" --unzip --cleanup --daystokeepbackup 180` + - **Start in**: `D:\AzureDevOpsBackup` + ## Logs Logs for backup jobs is keept in the **.\Log** folder for **30 days** beside **AzureDevOpsBackup.exe**. @@ -167,10 +218,18 @@ There is send an email report to the specified email address when the backup is Check out the examples here: +### AzureDevOpsBackup + `.\AzureDevOpsBackup.exe --token "xxxx..." --org "AzureDevOpsOrgName" --backup "D:\Backup data\Azure DevOps" --server "orgdomain-cloud.mail.protection.outlook.com" --port "25" --from "azure-devops-backup@orgdomain.cloud" --to "AZ-DL-AzureDevOpsBackupReports@orgdomain.cloud" --unzip --cleanup --daystokeepbackup 50` `.\AzureDevOpsBackup.exe --token "token.bin" --org "AzureDevOpsOrgName" --backup "D:\Backup data\Azure DevOps" --server "orgdomain-cloud.mail.protection.outlook.com" --port "25" --from "azure-devops-backup@orgdomain.cloud" --to "AZ-DL-AzureDevOpsBackupReports@orgdomain.cloud" --unzip --cleanup --daystokeepbackup 50 --simpelreportlayout --priority high` +`.\AzureDevOpsBackup.exe --token "token.bin" --org "AzureDevOpsOrgName" --backup "D:\Backup data\Azure DevOps" --server "orgdomain-cloud.mail.protection.outlook.com" --port "25" --from "azure-devops-backup@orgdomain.cloud" --to "admin@orgdomain.cloud,support@orgdomain.cloud" --unzip --cleanup --daystokeepbackup 120 --noattatchlog` + + +### AzureDevOpsBackup unzip tool +`.\AzureDevOpsBackupUnzipTool.exe --zipFile "C:\Temp\Test\master_blob.zip" --output "C:\Temp\Test\Test" --jsonFile "C:\Temp\Test\tree.json"` + # Console use: **Help menu:** @@ -224,6 +283,8 @@ Commercial support This project is open-source and I invite everybody who can and will to contribute, but I cannot provide any support because I only created this as a "hobby project" ofc. with tbe best in mind. For commercial support, please contact me on LinkedIn so we can discuss the possibilities. It’s my choice to work on this project in my spare time, so if you have commercial gain from this project you should considering sponsoring me. +Buy Me A Coffee + Thanks. Reach out to the maintainer at one of the following places: diff --git a/docs/help-menu.png b/docs/help-menu.png index fc8f2e6..48fdd5f 100644 Binary files a/docs/help-menu.png and b/docs/help-menu.png differ