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.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..4a41e94680 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.MinValue;
+
+ ///
+ /// Stores the date of the last backup of the settings file.
+ ///
+ public DateTime LastBackup
+ {
+ get => _lastBackup;
+ set
+ {
+ if (value == _lastBackup)
+ return;
+
+ _lastBackup = value;
+ OnPropertyChanged();
+ }
+ }
+
#region General
// General
@@ -1216,7 +1237,7 @@ public ExportFileType IPScanner_ExportFileType
#region Port Scanner
- private ObservableCollection _portScanner_HostHistory = new();
+ private ObservableCollection _portScanner_HostHistory = [];
public ObservableCollection PortScanner_HostHistory
{
@@ -1231,7 +1252,7 @@ public ObservableCollection PortScanner_HostHistory
}
}
- private ObservableCollection _portScanner_PortHistory = new();
+ private ObservableCollection _portScanner_PortHistory = [];
public ObservableCollection PortScanner_PortHistory
{
@@ -1246,7 +1267,7 @@ public ObservableCollection PortScanner_PortHistory
}
}
- private ObservableCollection _portScanner_PortProfiles = new();
+ private ObservableCollection _portScanner_PortProfiles = [];
public ObservableCollection PortScanner_PortProfiles
{
@@ -1400,7 +1421,7 @@ public ExportFileType PortScanner_ExportFileType
#region Ping Monitor
- private ObservableCollection _pingMonitor_HostHistory = new();
+ private ObservableCollection _pingMonitor_HostHistory = [];
public ObservableCollection PingMonitor_HostHistory
{
@@ -1569,7 +1590,7 @@ public double PingMonitor_ProfileWidth
#region Traceroute
- private ObservableCollection _traceroute_HostHistory = new();
+ private ObservableCollection _traceroute_HostHistory = [];
public ObservableCollection Traceroute_HostHistory
{
@@ -1999,7 +2020,7 @@ public ExportFileType DNSLookup_ExportFileType
#region Remote Desktop
- private ObservableCollection _remoteDesktop_HostHistory = new();
+ private ObservableCollection _remoteDesktop_HostHistory = [];
public ObservableCollection RemoteDesktop_HostHistory
{
@@ -2579,7 +2600,7 @@ public double RemoteDesktop_ProfileWidth
#region PowerShell
- private ObservableCollection _powerShell_HostHistory = new();
+ private ObservableCollection _powerShell_HostHistory = [];
public ObservableCollection PowerShell_HostHistory
{
@@ -2689,7 +2710,7 @@ public double PowerShell_ProfileWidth
#region PuTTY
- private ObservableCollection _puTTY_HostHistory = new();
+ private ObservableCollection _puTTY_HostHistory = [];
public ObservableCollection PuTTY_HostHistory
{
@@ -2839,7 +2860,7 @@ public string PuTTY_AdditionalCommandLine
}
}
- private ObservableCollection _puTTY_SerialLineHistory = new();
+ private ObservableCollection _puTTY_SerialLineHistory = [];
public ObservableCollection PuTTY_SerialLineHistory
{
@@ -2854,7 +2875,7 @@ public ObservableCollection PuTTY_SerialLineHistory
}
}
- private ObservableCollection _puTTY_PortHistory = new();
+ private ObservableCollection _puTTY_PortHistory = [];
public ObservableCollection PuTTY_PortHistory
{
@@ -2869,7 +2890,7 @@ public ObservableCollection PuTTY_PortHistory
}
}
- private ObservableCollection _puTTY_BaudHistory = new();
+ private ObservableCollection _puTTY_BaudHistory = [];
public ObservableCollection PuTTY_BaudHistory
{
@@ -2884,7 +2905,7 @@ public ObservableCollection PuTTY_BaudHistory
}
}
- private ObservableCollection _puTTY_UsernameHistory = new();
+ private ObservableCollection _puTTY_UsernameHistory = [];
public ObservableCollection PuTTY_UsernameHistory
{
@@ -2899,7 +2920,7 @@ public ObservableCollection PuTTY_UsernameHistory
}
}
- private ObservableCollection _puTTY_PrivateKeyFileHistory = new();
+ private ObservableCollection _puTTY_PrivateKeyFileHistory = [];
public ObservableCollection PuTTY_PrivateKeyFileHistory
{
@@ -2914,7 +2935,7 @@ public ObservableCollection PuTTY_PrivateKeyFileHistory
}
}
- private ObservableCollection _puTTY_ProfileHistory = new();
+ private ObservableCollection _puTTY_ProfileHistory = [];
public ObservableCollection PuTTY_ProfileHistory
{
@@ -3069,7 +3090,7 @@ public int PuTTY_RawPort
#region TigerVNC
- private ObservableCollection _tigerVNC_HostHistory = new();
+ private ObservableCollection _tigerVNC_HostHistory = [];
public ObservableCollection TigerVNC_HostHistory
{
@@ -3084,7 +3105,7 @@ public ObservableCollection TigerVNC_HostHistory
}
}
- private ObservableCollection _tigerVNC_PortHistory = new();
+ private ObservableCollection _tigerVNC_PortHistory = [];
public ObservableCollection TigerVNC_PortHistory
{
@@ -3163,7 +3184,7 @@ public int TigerVNC_Port
#region Web Console
- private ObservableCollection _webConsole_UrlHistory = new();
+ private ObservableCollection _webConsole_UrlHistory = [];
public ObservableCollection WebConsole_UrlHistory
{
@@ -3257,7 +3278,7 @@ public bool WebConsole_IsPasswordSaveEnabled
#region SNMP
- private ObservableCollection _snmp_HostHistory = new();
+ private ObservableCollection _snmp_HostHistory = [];
public ObservableCollection SNMP_HostHistory
{
@@ -3272,7 +3293,7 @@ public ObservableCollection SNMP_HostHistory
}
}
- private ObservableCollection _snmp_OidHistory = new();
+ private ObservableCollection _snmp_OidHistory = [];
public ObservableCollection SNMP_OidHistory
{
@@ -3287,7 +3308,7 @@ public ObservableCollection SNMP_OidHistory
}
}
- private ObservableCollection _snmp_OidProfiles = new();
+ private ObservableCollection _snmp_OidProfiles = [];
public ObservableCollection SNMP_OidProfiles
{
@@ -3681,7 +3702,7 @@ public int WakeOnLAN_Port
}
}
- private ObservableCollection _wakeOnLan_MACAddressHistory = new();
+ private ObservableCollection _wakeOnLan_MACAddressHistory = [];
public ObservableCollection WakeOnLan_MACAddressHistory
{
@@ -3696,7 +3717,7 @@ public ObservableCollection WakeOnLan_MACAddressHistory
}
}
- private ObservableCollection _wakeOnLan_BroadcastHistory = new();
+ private ObservableCollection _wakeOnLan_BroadcastHistory = [];
public ObservableCollection WakeOnLan_BroadcastHistory
{
@@ -3745,7 +3766,7 @@ public double WakeOnLAN_ProfileWidth
#region Whois
- private ObservableCollection _whois_DomainHistory = new();
+ private ObservableCollection _whois_DomainHistory = [];
public ObservableCollection Whois_DomainHistory
{
@@ -3824,7 +3845,7 @@ public ExportFileType Whois_ExportFileType
#region IP Geolocation
- private ObservableCollection _ipGeolocation_HostHistory = new();
+ private ObservableCollection _ipGeolocation_HostHistory = [];
public ObservableCollection IPGeolocation_HostHistory
{
@@ -3905,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
{
@@ -3924,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
{
@@ -3939,7 +3960,7 @@ public ObservableCollection SubnetCalculator_Subnetting_SubnetHistory
}
}
- private ObservableCollection _subnetCalculator_Subnetting_NewSubnetmaskHistory = new();
+ private ObservableCollection _subnetCalculator_Subnetting_NewSubnetmaskHistory = [];
public ObservableCollection SubnetCalculator_Subnetting_NewSubnetmaskHistory
{
@@ -3989,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
{
@@ -4004,7 +4025,7 @@ public ObservableCollection SubnetCalculator_WideSubnet_Subnet1
}
}
- private ObservableCollection _subnetCalculator_WideSubnet_Subnet2 = new();
+ private ObservableCollection _subnetCalculator_WideSubnet_Subnet2 = [];
public ObservableCollection SubnetCalculator_WideSubnet_Subnet2
{
@@ -4025,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 1ce2e093d4..61f95802ac 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(),
- $"{TimestampHelper.GetTimestamp()}_{GetLegacySettingsFileName()}");
-
- File.Copy(legacyFilePath, backupFilePath, true);
+ Backup(legacyFilePath,
+ GetSettingsBackupFolderLocation(),
+ TimestampHelper.GetTimestampFilename(GetLegacySettingsFileName()));
File.Delete(legacyFilePath);
- Log.Info($"Legacy XML settings file backed up to: {backupFilePath}");
-
Log.Info("Settings migration from XML to JSON completed successfully.");
return;
@@ -237,6 +232,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 +256,93 @@ 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()
{
- Log.Info("Creating settings backup...");
+ 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.GetTimestampFilename(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 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())) && 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();
+
+ 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 settingsFilePath, 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(settingsFilePath, backupFilePath, true);
+
+ Log.Info($"Backup created: {backupFilePath}");
}
- */
#endregion
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
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)
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/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**
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
:::