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
:::