From 493d62fbe827ef0d2b9e329f540eae0871ccab55 Mon Sep 17 00:00:00 2001 From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> Date: Thu, 25 Dec 2025 19:06:17 +0100 Subject: [PATCH 1/7] Feature: Create a daily settings backup --- .../GlobalStaticConfiguration.cs | 3 + .../NETworkManager.Settings/SettingsInfo.cs | 23 ++++- .../SettingsManager.cs | 89 +++++++++++++++++-- 3 files changed, 109 insertions(+), 6 deletions(-) diff --git a/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs b/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs index 48843d0237..af7ea9117d 100644 --- a/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs +++ b/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs @@ -46,6 +46,9 @@ public static class GlobalStaticConfiguration public static string ZipFileExtensionFilter => "ZIP Archive (*.zip)|*.zip"; public static string XmlFileExtensionFilter => "XML-File (*.xml)|*.xml"; + // Backup settings + public static int Backup_MaximumNumberOfBackups => 10; + #endregion #region Default settings diff --git a/Source/NETworkManager.Settings/SettingsInfo.cs b/Source/NETworkManager.Settings/SettingsInfo.cs index d3d5d4c83f..0a067af2d1 100644 --- a/Source/NETworkManager.Settings/SettingsInfo.cs +++ b/Source/NETworkManager.Settings/SettingsInfo.cs @@ -109,6 +109,27 @@ public string Version } } + /// + /// Private field for the property. + /// + private DateTime _lastBackup = DateTime.Now.Date; + + /// + /// Store the date and time of the last backup of the settings file. + /// + public DateTime LastBackup + { + get => _lastBackup; + set + { + if (value == _lastBackup) + return; + + _lastBackup = value; + OnPropertyChanged(); + } + } + #region General // General @@ -1400,7 +1421,7 @@ public ExportFileType PortScanner_ExportFileType #region Ping Monitor - private ObservableCollection _pingMonitor_HostHistory = new(); + private ObservableCollection _pingMonitor_HostHistory = []; public ObservableCollection PingMonitor_HostHistory { diff --git a/Source/NETworkManager.Settings/SettingsManager.cs b/Source/NETworkManager.Settings/SettingsManager.cs index 1ce2e093d4..b2f70de32c 100644 --- a/Source/NETworkManager.Settings/SettingsManager.cs +++ b/Source/NETworkManager.Settings/SettingsManager.cs @@ -237,6 +237,9 @@ public static void Save() // Create the directory if it does not exist Directory.CreateDirectory(GetSettingsFolderLocation()); + // Create backup before modifying + CreateDailyBackupIfNeeded(); + // Serialize the settings to a file SerializeToFile(GetSettingsFilePath()); @@ -258,15 +261,91 @@ private static void SerializeToFile(string filePath) #endregion #region Backup - /* - private static void Backup() + /// + /// Creates a backup of the settings file if a backup has not already been created for the current day. + /// + /// This method checks whether a backup for the current date exists and, if not, creates a new + /// backup of the settings file. It also removes old backups according to the configured maximum number of backups. + /// If the settings file does not exist, no backup is created and a warning is logged. This method is intended to be + /// called as part of a daily maintenance routine. + private static void CreateDailyBackupIfNeeded() + { + var currentDate = DateTime.Now.Date; + + if (Current.LastBackup < currentDate) + { + // Check if settings file exists + if (!File.Exists(GetSettingsFilePath())) + { + Log.Warn("Settings file does not exist yet. Skipping backup creation..."); + return; + } + + // Create backup + Backup(GetSettingsFilePath(), + GetSettingsBackupFolderLocation(), + $"{TimestampHelper.GetTimestamp()}_{GetSettingsFileName()}"); + + // Cleanup old backups + CleanupBackups(GetSettingsBackupFolderLocation(), + GetSettingsFileName(), + GlobalStaticConfiguration.Backup_MaximumNumberOfBackups); + + Current.LastBackup = currentDate; + } + } + + /// + /// Deletes older backup files in the specified folder to ensure that only the most recent backups, up to the + /// specified maximum, are retained. + /// + /// This method removes the oldest backup files first, keeping only the most recent backups as + /// determined by file creation time. It is intended to prevent excessive accumulation of backup files and manage + /// disk space usage. + /// The full path to the directory containing the backup files to be managed. Cannot be null or empty. + /// The file name pattern used to identify backup files for cleanup. + /// The maximum number of backup files to retain. Must be greater than zero. + private static void CleanupBackups(string backupFolderPath, string settingsFileName, int maxBackupFiles) { - Log.Info("Creating settings backup..."); + var backupFiles = Directory.GetFiles(backupFolderPath) + .Where(f => f.EndsWith(settingsFileName) || f.EndsWith(Path.Combine(GetLegacySettingsFileName()))) + .OrderByDescending(f => File.GetCreationTime(f)) + .ToList(); + + if (backupFiles.Count > maxBackupFiles) + Log.Info($"Cleaning up old backup files... Found {backupFiles.Count} backups, keeping the most recent {maxBackupFiles}."); + + while (backupFiles.Count > maxBackupFiles) + { + var fileToDelete = backupFiles.Last(); + File.Delete(fileToDelete); + + backupFiles.RemoveAt(backupFiles.Count - 1); + + Log.Info($"Backup deleted: {fileToDelete}"); + } + } + + /// + /// Creates a backup of the specified settings file in the given backup folder with the provided backup file name. + /// + /// The full path to the settings file to back up. Cannot be null or empty. + /// The directory path where the backup file will be stored. If the directory does not exist, it will be created. + /// The name to use for the backup file within the backup folder. Cannot be null or empty. + private static void Backup(string setingsFilePath, string backupFolderPath, string backupFileName) + { // Create the backup directory if it does not exist - Directory.CreateDirectory(GetSettingsBackupFolderLocation()); + Directory.CreateDirectory(backupFolderPath); + + // Create the backup file path + var backupFilePath = Path.Combine(backupFolderPath, backupFileName); + + // Copy the current settings file to the backup location + File.Copy(setingsFilePath, backupFilePath, true); + + Log.Info($"Backup created: {backupFilePath}"); } - */ #endregion From 40fbae02096e2c81bb95bfc12fa3a1e2d86d0344 Mon Sep 17 00:00:00 2001 From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> Date: Thu, 25 Dec 2025 19:10:11 +0100 Subject: [PATCH 2/7] Chore: Cleanup --- Source/NETworkManager.Settings/SettingsManager.cs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/Source/NETworkManager.Settings/SettingsManager.cs b/Source/NETworkManager.Settings/SettingsManager.cs index b2f70de32c..7f2e2a23c6 100644 --- a/Source/NETworkManager.Settings/SettingsManager.cs +++ b/Source/NETworkManager.Settings/SettingsManager.cs @@ -178,17 +178,12 @@ public static void Load() Save(); // Create a backup of the legacy XML file and delete the original - Directory.CreateDirectory(GetSettingsBackupFolderLocation()); - - var backupFilePath = Path.Combine(GetSettingsBackupFolderLocation(), + Backup(legacyFilePath, + GetSettingsBackupFolderLocation(), $"{TimestampHelper.GetTimestamp()}_{GetLegacySettingsFileName()}"); - - File.Copy(legacyFilePath, backupFilePath, true); - + File.Delete(legacyFilePath); - Log.Info($"Legacy XML settings file backed up to: {backupFilePath}"); - Log.Info("Settings migration from XML to JSON completed successfully."); return; From ffcc4eba8395b75dc2f81fe5ffac43970ef930f4 Mon Sep 17 00:00:00 2001 From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> Date: Thu, 25 Dec 2025 19:13:11 +0100 Subject: [PATCH 3/7] Docs: #3283 --- Website/docs/changelog/next-release.md | 1 + 1 file changed, 1 insertion(+) diff --git a/Website/docs/changelog/next-release.md b/Website/docs/changelog/next-release.md index 417fc79f9c..ba441a511c 100644 --- a/Website/docs/changelog/next-release.md +++ b/Website/docs/changelog/next-release.md @@ -54,6 +54,7 @@ Release date: **xx.xx.2025** **Settings** - Settings format migrated from `XML` to `JSON`. The settings file will be automatically converted on first start after the update. [#3282](https://github.com/BornToBeRoot/NETworkManager/pull/3282) +- Create a daily backup of the settings file before saving changes. Up to `10` backup files are kept in the `Backups` subfolder of the settings directory. [#3283](https://github.com/BornToBeRoot/NETworkManager/pull/3283) **DNS Lookup** From c48db9a2fabe824545669748a589140e5927130c Mon Sep 17 00:00:00 2001 From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> Date: Thu, 25 Dec 2025 19:15:33 +0100 Subject: [PATCH 4/7] Chore: Cleanup --- .../NETworkManager.Settings/SettingsInfo.cs | 60 +++++++++---------- .../SettingsManager.cs | 2 +- Source/NETworkManager/App.xaml.cs | 2 +- 3 files changed, 32 insertions(+), 32 deletions(-) diff --git a/Source/NETworkManager.Settings/SettingsInfo.cs b/Source/NETworkManager.Settings/SettingsInfo.cs index 0a067af2d1..1f613e54e4 100644 --- a/Source/NETworkManager.Settings/SettingsInfo.cs +++ b/Source/NETworkManager.Settings/SettingsInfo.cs @@ -124,7 +124,7 @@ public DateTime LastBackup { if (value == _lastBackup) return; - + _lastBackup = value; OnPropertyChanged(); } @@ -1237,7 +1237,7 @@ public ExportFileType IPScanner_ExportFileType #region Port Scanner - private ObservableCollection _portScanner_HostHistory = new(); + private ObservableCollection _portScanner_HostHistory = []; public ObservableCollection PortScanner_HostHistory { @@ -1252,7 +1252,7 @@ public ObservableCollection PortScanner_HostHistory } } - private ObservableCollection _portScanner_PortHistory = new(); + private ObservableCollection _portScanner_PortHistory = []; public ObservableCollection PortScanner_PortHistory { @@ -1267,7 +1267,7 @@ public ObservableCollection PortScanner_PortHistory } } - private ObservableCollection _portScanner_PortProfiles = new(); + private ObservableCollection _portScanner_PortProfiles = []; public ObservableCollection PortScanner_PortProfiles { @@ -1590,7 +1590,7 @@ public double PingMonitor_ProfileWidth #region Traceroute - private ObservableCollection _traceroute_HostHistory = new(); + private ObservableCollection _traceroute_HostHistory = []; public ObservableCollection Traceroute_HostHistory { @@ -2020,7 +2020,7 @@ public ExportFileType DNSLookup_ExportFileType #region Remote Desktop - private ObservableCollection _remoteDesktop_HostHistory = new(); + private ObservableCollection _remoteDesktop_HostHistory = []; public ObservableCollection RemoteDesktop_HostHistory { @@ -2600,7 +2600,7 @@ public double RemoteDesktop_ProfileWidth #region PowerShell - private ObservableCollection _powerShell_HostHistory = new(); + private ObservableCollection _powerShell_HostHistory = []; public ObservableCollection PowerShell_HostHistory { @@ -2710,7 +2710,7 @@ public double PowerShell_ProfileWidth #region PuTTY - private ObservableCollection _puTTY_HostHistory = new(); + private ObservableCollection _puTTY_HostHistory = []; public ObservableCollection PuTTY_HostHistory { @@ -2860,7 +2860,7 @@ public string PuTTY_AdditionalCommandLine } } - private ObservableCollection _puTTY_SerialLineHistory = new(); + private ObservableCollection _puTTY_SerialLineHistory = []; public ObservableCollection PuTTY_SerialLineHistory { @@ -2875,7 +2875,7 @@ public ObservableCollection PuTTY_SerialLineHistory } } - private ObservableCollection _puTTY_PortHistory = new(); + private ObservableCollection _puTTY_PortHistory = []; public ObservableCollection PuTTY_PortHistory { @@ -2890,7 +2890,7 @@ public ObservableCollection PuTTY_PortHistory } } - private ObservableCollection _puTTY_BaudHistory = new(); + private ObservableCollection _puTTY_BaudHistory = []; public ObservableCollection PuTTY_BaudHistory { @@ -2905,7 +2905,7 @@ public ObservableCollection PuTTY_BaudHistory } } - private ObservableCollection _puTTY_UsernameHistory = new(); + private ObservableCollection _puTTY_UsernameHistory = []; public ObservableCollection PuTTY_UsernameHistory { @@ -2920,7 +2920,7 @@ public ObservableCollection PuTTY_UsernameHistory } } - private ObservableCollection _puTTY_PrivateKeyFileHistory = new(); + private ObservableCollection _puTTY_PrivateKeyFileHistory = []; public ObservableCollection PuTTY_PrivateKeyFileHistory { @@ -2935,7 +2935,7 @@ public ObservableCollection PuTTY_PrivateKeyFileHistory } } - private ObservableCollection _puTTY_ProfileHistory = new(); + private ObservableCollection _puTTY_ProfileHistory = []; public ObservableCollection PuTTY_ProfileHistory { @@ -3090,7 +3090,7 @@ public int PuTTY_RawPort #region TigerVNC - private ObservableCollection _tigerVNC_HostHistory = new(); + private ObservableCollection _tigerVNC_HostHistory = []; public ObservableCollection TigerVNC_HostHistory { @@ -3105,7 +3105,7 @@ public ObservableCollection TigerVNC_HostHistory } } - private ObservableCollection _tigerVNC_PortHistory = new(); + private ObservableCollection _tigerVNC_PortHistory = []; public ObservableCollection TigerVNC_PortHistory { @@ -3184,7 +3184,7 @@ public int TigerVNC_Port #region Web Console - private ObservableCollection _webConsole_UrlHistory = new(); + private ObservableCollection _webConsole_UrlHistory = []; public ObservableCollection WebConsole_UrlHistory { @@ -3278,7 +3278,7 @@ public bool WebConsole_IsPasswordSaveEnabled #region SNMP - private ObservableCollection _snmp_HostHistory = new(); + private ObservableCollection _snmp_HostHistory = []; public ObservableCollection SNMP_HostHistory { @@ -3293,7 +3293,7 @@ public ObservableCollection SNMP_HostHistory } } - private ObservableCollection _snmp_OidHistory = new(); + private ObservableCollection _snmp_OidHistory = []; public ObservableCollection SNMP_OidHistory { @@ -3308,7 +3308,7 @@ public ObservableCollection SNMP_OidHistory } } - private ObservableCollection _snmp_OidProfiles = new(); + private ObservableCollection _snmp_OidProfiles = []; public ObservableCollection SNMP_OidProfiles { @@ -3702,7 +3702,7 @@ public int WakeOnLAN_Port } } - private ObservableCollection _wakeOnLan_MACAddressHistory = new(); + private ObservableCollection _wakeOnLan_MACAddressHistory = []; public ObservableCollection WakeOnLan_MACAddressHistory { @@ -3717,7 +3717,7 @@ public ObservableCollection WakeOnLan_MACAddressHistory } } - private ObservableCollection _wakeOnLan_BroadcastHistory = new(); + private ObservableCollection _wakeOnLan_BroadcastHistory = []; public ObservableCollection WakeOnLan_BroadcastHistory { @@ -3766,7 +3766,7 @@ public double WakeOnLAN_ProfileWidth #region Whois - private ObservableCollection _whois_DomainHistory = new(); + private ObservableCollection _whois_DomainHistory = []; public ObservableCollection Whois_DomainHistory { @@ -3845,7 +3845,7 @@ public ExportFileType Whois_ExportFileType #region IP Geolocation - private ObservableCollection _ipGeolocation_HostHistory = new(); + private ObservableCollection _ipGeolocation_HostHistory = []; public ObservableCollection IPGeolocation_HostHistory { @@ -3926,7 +3926,7 @@ public ExportFileType IPGeolocation_ExportFileType #region Calculator - private ObservableCollection _subnetCalculator_Calculator_SubnetHistory = new(); + private ObservableCollection _subnetCalculator_Calculator_SubnetHistory = []; public ObservableCollection SubnetCalculator_Calculator_SubnetHistory { @@ -3945,7 +3945,7 @@ public ObservableCollection SubnetCalculator_Calculator_SubnetHistory #region Subnetting - private ObservableCollection _subnetCalculator_Subnetting_SubnetHistory = new(); + private ObservableCollection _subnetCalculator_Subnetting_SubnetHistory = []; public ObservableCollection SubnetCalculator_Subnetting_SubnetHistory { @@ -3960,7 +3960,7 @@ public ObservableCollection SubnetCalculator_Subnetting_SubnetHistory } } - private ObservableCollection _subnetCalculator_Subnetting_NewSubnetmaskHistory = new(); + private ObservableCollection _subnetCalculator_Subnetting_NewSubnetmaskHistory = []; public ObservableCollection SubnetCalculator_Subnetting_NewSubnetmaskHistory { @@ -4010,7 +4010,7 @@ public ExportFileType SubnetCalculator_Subnetting_ExportFileType #region WideSubnet - private ObservableCollection _subnetCalculator_WideSubnet_Subnet1 = new(); + private ObservableCollection _subnetCalculator_WideSubnet_Subnet1 = []; public ObservableCollection SubnetCalculator_WideSubnet_Subnet1 { @@ -4025,7 +4025,7 @@ public ObservableCollection SubnetCalculator_WideSubnet_Subnet1 } } - private ObservableCollection _subnetCalculator_WideSubnet_Subnet2 = new(); + private ObservableCollection _subnetCalculator_WideSubnet_Subnet2 = []; public ObservableCollection SubnetCalculator_WideSubnet_Subnet2 { @@ -4046,7 +4046,7 @@ public ObservableCollection SubnetCalculator_WideSubnet_Subnet2 #region Bit Calculator - private ObservableCollection _bitCalculator_InputHistory = new(); + private ObservableCollection _bitCalculator_InputHistory = []; public ObservableCollection BitCalculator_InputHistory { diff --git a/Source/NETworkManager.Settings/SettingsManager.cs b/Source/NETworkManager.Settings/SettingsManager.cs index 7f2e2a23c6..5341909856 100644 --- a/Source/NETworkManager.Settings/SettingsManager.cs +++ b/Source/NETworkManager.Settings/SettingsManager.cs @@ -181,7 +181,7 @@ public static void Load() Backup(legacyFilePath, GetSettingsBackupFolderLocation(), $"{TimestampHelper.GetTimestamp()}_{GetLegacySettingsFileName()}"); - + File.Delete(legacyFilePath); Log.Info("Settings migration from XML to JSON completed successfully."); diff --git a/Source/NETworkManager/App.xaml.cs b/Source/NETworkManager/App.xaml.cs index ace2a1267f..b3e4cd3f1f 100644 --- a/Source/NETworkManager/App.xaml.cs +++ b/Source/NETworkManager/App.xaml.cs @@ -94,7 +94,7 @@ by BornToBeRoot catch (InvalidOperationException ex) { Log.Error("Could not load application settings!", ex); - + HandleCorruptedSettingsFile(); } catch (JsonException ex) From 39ac33e0b3a07cb0c6a657203f14de46ddafc793 Mon Sep 17 00:00:00 2001 From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> Date: Thu, 25 Dec 2025 19:20:46 +0100 Subject: [PATCH 5/7] Fix: Fixes based on copilot feedback --- Source/NETworkManager.Settings/SettingsInfo.cs | 4 ++-- Source/NETworkManager.Settings/SettingsManager.cs | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Source/NETworkManager.Settings/SettingsInfo.cs b/Source/NETworkManager.Settings/SettingsInfo.cs index 1f613e54e4..4a41e94680 100644 --- a/Source/NETworkManager.Settings/SettingsInfo.cs +++ b/Source/NETworkManager.Settings/SettingsInfo.cs @@ -112,10 +112,10 @@ public string Version /// /// Private field for the property. /// - private DateTime _lastBackup = DateTime.Now.Date; + private DateTime _lastBackup = DateTime.MinValue; /// - /// Store the date and time of the last backup of the settings file. + /// Stores the date of the last backup of the settings file. /// public DateTime LastBackup { diff --git a/Source/NETworkManager.Settings/SettingsManager.cs b/Source/NETworkManager.Settings/SettingsManager.cs index 5341909856..83652a1866 100644 --- a/Source/NETworkManager.Settings/SettingsManager.cs +++ b/Source/NETworkManager.Settings/SettingsManager.cs @@ -303,7 +303,7 @@ private static void CreateDailyBackupIfNeeded() private static void CleanupBackups(string backupFolderPath, string settingsFileName, int maxBackupFiles) { var backupFiles = Directory.GetFiles(backupFolderPath) - .Where(f => f.EndsWith(settingsFileName) || f.EndsWith(Path.Combine(GetLegacySettingsFileName()))) + .Where(f => f.EndsWith(settingsFileName) || f.EndsWith(GetLegacySettingsFileName())) .OrderByDescending(f => File.GetCreationTime(f)) .ToList(); @@ -325,10 +325,10 @@ private static void CleanupBackups(string backupFolderPath, string settingsFileN /// /// Creates a backup of the specified settings file in the given backup folder with the provided backup file name. /// - /// The full path to the settings file to back up. Cannot be null or empty. + /// The full path to the settings file to back up. Cannot be null or empty. /// The directory path where the backup file will be stored. If the directory does not exist, it will be created. /// The name to use for the backup file within the backup folder. Cannot be null or empty. - private static void Backup(string setingsFilePath, string backupFolderPath, string backupFileName) + private static void Backup(string settingsFilePath, string backupFolderPath, string backupFileName) { // Create the backup directory if it does not exist Directory.CreateDirectory(backupFolderPath); @@ -337,7 +337,7 @@ private static void Backup(string setingsFilePath, string backupFolderPath, stri var backupFilePath = Path.Combine(backupFolderPath, backupFileName); // Copy the current settings file to the backup location - File.Copy(setingsFilePath, backupFilePath, true); + File.Copy(settingsFilePath, backupFilePath, true); Log.Info($"Backup created: {backupFilePath}"); } From 35e88cc3acd306189f3c4379ee218bc22281c87a Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 25 Dec 2025 23:21:33 +0100 Subject: [PATCH 6/7] Use filename timestamp for backup ordering and move to Utilities (#3284) * Initial plan * Extract date from filename for backup ordering Co-authored-by: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> * Use InvariantCulture and specific exception handling Co-authored-by: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> * Move ExtractTimestampFromFilename to TimestampHelper in Utilities Co-authored-by: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> * Update comment to match specific exception handling Co-authored-by: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> * Feature: Improve check * Update Source/NETworkManager.Utilities/TimestampHelper.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Source/NETworkManager.Utilities/TimestampHelper.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Source/NETworkManager.Utilities/TimestampHelper.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update TimestampHelper.cs * Update TimestampHelper.cs --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../SettingsManager.cs | 12 +++-- .../TimestampHelper.cs | 46 +++++++++++++++++++ 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/Source/NETworkManager.Settings/SettingsManager.cs b/Source/NETworkManager.Settings/SettingsManager.cs index 83652a1866..61f95802ac 100644 --- a/Source/NETworkManager.Settings/SettingsManager.cs +++ b/Source/NETworkManager.Settings/SettingsManager.cs @@ -180,7 +180,7 @@ public static void Load() // Create a backup of the legacy XML file and delete the original Backup(legacyFilePath, GetSettingsBackupFolderLocation(), - $"{TimestampHelper.GetTimestamp()}_{GetLegacySettingsFileName()}"); + TimestampHelper.GetTimestampFilename(GetLegacySettingsFileName())); File.Delete(legacyFilePath); @@ -279,7 +279,7 @@ private static void CreateDailyBackupIfNeeded() // Create backup Backup(GetSettingsFilePath(), GetSettingsBackupFolderLocation(), - $"{TimestampHelper.GetTimestamp()}_{GetSettingsFileName()}"); + TimestampHelper.GetTimestampFilename(GetSettingsFileName())); // Cleanup old backups CleanupBackups(GetSettingsBackupFolderLocation(), @@ -295,21 +295,23 @@ private static void CreateDailyBackupIfNeeded() /// specified maximum, are retained. /// /// This method removes the oldest backup files first, keeping only the most recent backups as - /// determined by file creation time. It is intended to prevent excessive accumulation of backup files and manage + /// determined by the timestamp in the filename. It is intended to prevent excessive accumulation of backup files and manage /// disk space usage. /// The full path to the directory containing the backup files to be managed. Cannot be null or empty. /// The file name pattern used to identify backup files for cleanup. /// The maximum number of backup files to retain. Must be greater than zero. private static void CleanupBackups(string backupFolderPath, string settingsFileName, int maxBackupFiles) { + // Get all backup files sorted by timestamp (newest first) var backupFiles = Directory.GetFiles(backupFolderPath) - .Where(f => f.EndsWith(settingsFileName) || f.EndsWith(GetLegacySettingsFileName())) - .OrderByDescending(f => File.GetCreationTime(f)) + .Where(f => (f.EndsWith(settingsFileName) || f.EndsWith(GetLegacySettingsFileName())) && TimestampHelper.IsTimestampedFilename(Path.GetFileName(f))) + .OrderByDescending(f => TimestampHelper.ExtractTimestampFromFilename(Path.GetFileName(f))) .ToList(); if (backupFiles.Count > maxBackupFiles) Log.Info($"Cleaning up old backup files... Found {backupFiles.Count} backups, keeping the most recent {maxBackupFiles}."); + // Delete oldest backups until the maximum number is reached while (backupFiles.Count > maxBackupFiles) { var fileToDelete = backupFiles.Last(); diff --git a/Source/NETworkManager.Utilities/TimestampHelper.cs b/Source/NETworkManager.Utilities/TimestampHelper.cs index 624f082b05..0ed4b5b83b 100644 --- a/Source/NETworkManager.Utilities/TimestampHelper.cs +++ b/Source/NETworkManager.Utilities/TimestampHelper.cs @@ -1,4 +1,6 @@ using System; +using System.Globalization; +using System.IO; namespace NETworkManager.Utilities; @@ -8,4 +10,48 @@ public static string GetTimestamp() { return DateTime.Now.ToString("yyyyMMddHHmmss"); } + + /// + /// Generates a filename by prefixing the specified filename with a timestamp string. + /// + /// The original filename to be prefixed with a timestamp. Cannot be null or empty. + /// A string containing the timestamp followed by an underscore and the original filename. + public static string GetTimestampFilename(string fileName) + { + return $"{GetTimestamp()}_{fileName}"; + } + + /// + /// Determines whether the specified file name begins with a valid timestamp in the format "yyyyMMddHHmmss". + /// + /// This method checks only the first 14 characters of the file name for a valid timestamp and + /// does not validate the remainder of the file name. + /// The file name to evaluate. The file name is expected to start with a 14-digit timestamp followed by an + /// underscore and at least one additional character. + /// true if the file name starts with a valid timestamp in the format "yyyyMMddHHmmss"; otherwise, false. + public static bool IsTimestampedFilename(string fileName) + { + // Ensure the filename is long enough to contain a timestamp, an underscore, and at least one character after it + if (fileName.Length < 16) + return false; + + var timestampString = fileName.Substring(0, 14); + + return DateTime.TryParseExact(timestampString, "yyyyMMddHHmmss", CultureInfo.InvariantCulture, DateTimeStyles.None, out _); + } + + /// + /// Extracts the timestamp from a filename that starts with a timestamp prefix. + /// + /// Filenames are expected to start with yyyyMMddHHmmss_* format (14 characters). + /// This method extracts the timestamp portion and parses it as a DateTime. + /// The full path to the file or just the filename. + /// The timestamp extracted from the filename. + public static DateTime ExtractTimestampFromFilename(string fileName) + { + // Extract the timestamp prefix (yyyyMMddHHmmss format, 14 characters) + var timestampString = fileName.Substring(0, 14); + + return DateTime.ParseExact(timestampString, "yyyyMMddHHmmss", CultureInfo.InvariantCulture); + } } \ No newline at end of file From b7bb7350f673654c1a65ba1b6d87cd8f2a3064a7 Mon Sep 17 00:00:00 2001 From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> Date: Thu, 25 Dec 2025 23:52:47 +0100 Subject: [PATCH 7/7] Feature: Some logging / docs --- .../NETworkManager.Profiles/ProfileManager.cs | 37 +++++++++++-------- .../ViewModels/PingMonitorHostViewModel.cs | 4 +- Website/docs/settings/settings.md | 17 ++++++++- 3 files changed, 39 insertions(+), 19 deletions(-) diff --git a/Source/NETworkManager.Profiles/ProfileManager.cs b/Source/NETworkManager.Profiles/ProfileManager.cs index 71e02e49e3..084b831f35 100644 --- a/Source/NETworkManager.Profiles/ProfileManager.cs +++ b/Source/NETworkManager.Profiles/ProfileManager.cs @@ -1,4 +1,6 @@ -using NETworkManager.Settings; +using log4net; +using NETworkManager.Models.Network; +using NETworkManager.Settings; using NETworkManager.Utilities; using System; using System.Collections.Generic; @@ -13,19 +15,8 @@ namespace NETworkManager.Profiles; public static class ProfileManager { - #region Constructor - - /// - /// Static constructor. Load all profile files on startup. - /// - static ProfileManager() - { - LoadProfileFiles(); - } - - #endregion - #region Variables + private static readonly ILog Log = LogManager.GetLogger(typeof(ProfileManager)); /// /// Profiles directory name. @@ -84,6 +75,18 @@ private set #endregion + #region Constructor + + /// + /// Static constructor. Load all profile files on startup. + /// + static ProfileManager() + { + LoadProfileFiles(); + } + + #endregion + #region Events /// @@ -202,7 +205,7 @@ public static void CreateEmptyProfileFile(string profileName) Directory.CreateDirectory(GetProfilesFolderLocation()); - SerializeToFile(profileFileInfo.Path, new List()); + SerializeToFile(profileFileInfo.Path, []); ProfileFiles.Add(profileFileInfo); } @@ -219,7 +222,6 @@ public static void RenameProfileFile(ProfileFileInfo profileFileInfo, string new if (LoadedProfileFile != null && LoadedProfileFile.Equals(profileFileInfo)) { Save(); - switchProfile = true; } @@ -472,7 +474,12 @@ private static void Load(ProfileFileInfo profileFileInfo) public static void Save() { if (LoadedProfileFile == null) + { + Log.Warn("Cannot save profiles because no profile file is loaded. The profile file may be encrypted and not yet unlocked."); + return; + } + Directory.CreateDirectory(GetProfilesFolderLocation()); diff --git a/Source/NETworkManager/ViewModels/PingMonitorHostViewModel.cs b/Source/NETworkManager/ViewModels/PingMonitorHostViewModel.cs index 12918d0ba7..6cc3c66e37 100644 --- a/Source/NETworkManager/ViewModels/PingMonitorHostViewModel.cs +++ b/Source/NETworkManager/ViewModels/PingMonitorHostViewModel.cs @@ -760,7 +760,7 @@ private void RemoveHostByGuid(Guid hostId) private void AddHostToHistory(string host) { // Create the new list - var list = ListHelper.Modify(SettingsManager.Current.PingMonitor_HostHistory.ToList(), host, + var list = ListHelper.Modify([.. SettingsManager.Current.PingMonitor_HostHistory], host, SettingsManager.Current.General_HistoryListEntries); // Clear the old items @@ -768,7 +768,7 @@ private void AddHostToHistory(string host) OnPropertyChanged(nameof(Host)); // Raise property changed again, after the collection has been cleared // Fill with the new items - list.ForEach(x => SettingsManager.Current.PingMonitor_HostHistory.Add(x)); + list.ForEach(SettingsManager.Current.PingMonitor_HostHistory.Add); } private void SetIsExpandedForAllProfileGroups(bool isExpanded) diff --git a/Website/docs/settings/settings.md b/Website/docs/settings/settings.md index 5bf0b9e593..f772085232 100644 --- a/Website/docs/settings/settings.md +++ b/Website/docs/settings/settings.md @@ -18,9 +18,22 @@ Folder where the application settings are stored. | Portable | `\Settings` | :::note -It is recommended to backup the above files on a regular basis. -To restore the settings, close the application and copy the files from the backup to the above location. +**Recommendation** +It is strongly recommended to regularly back up your settings files. + +**Automatic backups** +NETworkManager automatically creates a backup of the settings files before applying any changes. +- Location: `Settings\Backups` subfolder (relative to the main configuration directory) +- Naming: timestamped (e.g. `yyyyMMddHHmmss_Settings.json`) +- Frequency: **once per day** at most (even if multiple changes occur) +- Retention: keeps the **10 most recent backups** + +**Restoring settings** +1. Completely close NETworkManager +2. Locate the desired backup in `Settings\Backups` +3. Copy the file(s) back to the original configuration folder (overwriting existing files) +4. Restart the application :::