From 7fa5f4a4051623151d5292bc4d34073244930734 Mon Sep 17 00:00:00 2001
From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com>
Date: Sun, 11 Jan 2026 22:01:50 +0100
Subject: [PATCH 1/4] Feature: Configure backup retention
---
.../Resources/Strings.Designer.cs | 22 +++++++++++++
.../Resources/Strings.resx | 10 ++++++
.../Network/NetworkInterface.cs | 1 -
.../GlobalStaticConfiguration.cs | 16 +++++-----
.../NETworkManager.Settings/SettingsInfo.cs | 31 +++++++++++++++++++
.../SettingsManager.cs | 12 ++++++-
.../ViewModels/SettingsProfilesViewModel.cs | 24 ++++++++++++++
.../ViewModels/SettingsSettingsViewModel.cs | 30 ++++++++++++++++--
.../Views/SettingsSettingsView.xaml | 18 ++++++++++-
9 files changed, 150 insertions(+), 14 deletions(-)
diff --git a/Source/NETworkManager.Localization/Resources/Strings.Designer.cs b/Source/NETworkManager.Localization/Resources/Strings.Designer.cs
index fbd0f19ec1..4e7e0aca64 100644
--- a/Source/NETworkManager.Localization/Resources/Strings.Designer.cs
+++ b/Source/NETworkManager.Localization/Resources/Strings.Designer.cs
@@ -4552,6 +4552,19 @@ public static string HelpMessage_SaveCredentials {
}
}
+ ///
+ /// Sucht eine lokalisierte Zeichenfolge, die The number of settings backups that are retained before the oldest one is deleted.
+ ///
+ ///An automatic backup is only created once a day, before saving a change.
+ ///
+ ///The value 0 disables the creation of automatic backups. Backups that have already been created are not deleted. ähnelt.
+ ///
+ public static string HelpMessage_SettingsMaximumNumberOfBackups {
+ get {
+ return ResourceManager.GetString("HelpMessage_SettingsMaximumNumberOfBackups", resourceCulture);
+ }
+ }
+
///
/// Sucht eine lokalisierte Zeichenfolge, die Displays the status bar in the bottom-left of the WebView when hovering over a link. ähnelt.
///
@@ -5783,6 +5796,15 @@ public static string MaximumHops {
}
}
+ ///
+ /// Sucht eine lokalisierte Zeichenfolge, die Maximum Number of Backups ähnelt.
+ ///
+ public static string MaximumNumberOfBackups {
+ get {
+ return ResourceManager.GetString("MaximumNumberOfBackups", resourceCulture);
+ }
+ }
+
///
/// Sucht eine lokalisierte Zeichenfolge, die Maximum number ({0}) of hops/router reached! ähnelt.
///
diff --git a/Source/NETworkManager.Localization/Resources/Strings.resx b/Source/NETworkManager.Localization/Resources/Strings.resx
index 28ccf1c004..14ca461f78 100644
--- a/Source/NETworkManager.Localization/Resources/Strings.resx
+++ b/Source/NETworkManager.Localization/Resources/Strings.resx
@@ -3951,4 +3951,14 @@ If you click Cancel, the profile file will remain unencrypted.
Could not parse "{0}".
+
+ Maximum Number of Backups
+
+
+ The number of settings backups that are retained before the oldest one is deleted.
+
+An automatic backup is only created once a day, before saving a change.
+
+The value 0 disables the creation of automatic backups. Backups that have already been created are not deleted.
+
\ No newline at end of file
diff --git a/Source/NETworkManager.Models/Network/NetworkInterface.cs b/Source/NETworkManager.Models/Network/NetworkInterface.cs
index d7662f6704..7cd8c34038 100644
--- a/Source/NETworkManager.Models/Network/NetworkInterface.cs
+++ b/Source/NETworkManager.Models/Network/NetworkInterface.cs
@@ -527,7 +527,6 @@ private static void RemoveIPAddressFromNetworkInterface(NetworkInterfaceConfig c
#endregion
-
#region Events
///
diff --git a/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs b/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs
index af7ea9117d..28c92928a6 100644
--- a/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs
+++ b/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs
@@ -46,9 +46,6 @@ 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
@@ -74,18 +71,21 @@ public static class GlobalStaticConfiguration
public static bool Status_ShowWindowOnNetworkChange => true;
public static int Status_WindowCloseTime => 10;
- // HotKey
+ // Settings: HotKey
public static int HotKey_ShowWindowKey => 79;
public static int HotKey_ShowWindowModifier => 3;
- // Update
+ // Settings: Update
public static bool Update_CheckForUpdatesAtStartup => true;
-
public static bool Update_CheckForPreReleases => false;
-
- // Experimental
public static bool Experimental_EnableExperimentalFeatures => false;
+ // Settings: Profiles
+ public static int Profiles_MaximumNumberOfBackups => 10;
+
+ // Settings: Settings
+ public static int Settings_MaximumNumberOfBackups => 10;
+
// Application: Dashboard
public static string Dashboard_PublicIPv4Address => "1.1.1.1";
public static string Dashboard_PublicIPv6Address => "2606:4700:4700::1111";
diff --git a/Source/NETworkManager.Settings/SettingsInfo.cs b/Source/NETworkManager.Settings/SettingsInfo.cs
index 4a41e94680..b8f993cff0 100644
--- a/Source/NETworkManager.Settings/SettingsInfo.cs
+++ b/Source/NETworkManager.Settings/SettingsInfo.cs
@@ -582,6 +582,37 @@ public string Profiles_LastSelected
}
}
+ private int _profiles_MaximumNumberOfBackups = GlobalStaticConfiguration.Profiles_MaximumNumberOfBackups;
+
+ public int Profiles_MaximumNumberOfBackups
+ {
+ get => _profiles_MaximumNumberOfBackups;
+ set
+ {
+ if (value == _profiles_MaximumNumberOfBackups)
+ return;
+
+ _profiles_MaximumNumberOfBackups = value;
+ OnPropertyChanged();
+ }
+ }
+
+ // Settings
+ private int _settings_MaximumNumberOfBackups = GlobalStaticConfiguration.Settings_MaximumNumberOfBackups;
+
+ public int Settings_MaximumNumberOfBackups
+ {
+ get => _settings_MaximumNumberOfBackups;
+ set
+ {
+ if (value == _settings_MaximumNumberOfBackups)
+ return;
+
+ _settings_MaximumNumberOfBackups = value;
+ OnPropertyChanged();
+ }
+ }
+
#endregion
#region Others
diff --git a/Source/NETworkManager.Settings/SettingsManager.cs b/Source/NETworkManager.Settings/SettingsManager.cs
index 7c0041f063..6aeb6b9a96 100644
--- a/Source/NETworkManager.Settings/SettingsManager.cs
+++ b/Source/NETworkManager.Settings/SettingsManager.cs
@@ -265,6 +265,16 @@ private static void SerializeToFile(string filePath)
/// called as part of a daily maintenance routine.
private static void CreateDailyBackupIfNeeded()
{
+ var maxBackups = Current.Settings_MaximumNumberOfBackups;
+
+ // Check if backups are disabled
+ if (maxBackups == 0)
+ {
+ Log.Debug("Daily backups are disabled. Skipping backup creation...");
+
+ return;
+ }
+
var currentDate = DateTime.Now.Date;
if (Current.LastBackup < currentDate)
@@ -284,7 +294,7 @@ private static void CreateDailyBackupIfNeeded()
// Cleanup old backups
CleanupBackups(GetSettingsBackupFolderLocation(),
GetSettingsFileName(),
- GlobalStaticConfiguration.Backup_MaximumNumberOfBackups);
+ maxBackups);
Current.LastBackup = currentDate;
}
diff --git a/Source/NETworkManager/ViewModels/SettingsProfilesViewModel.cs b/Source/NETworkManager/ViewModels/SettingsProfilesViewModel.cs
index fd42c76821..04f4b7fcb1 100644
--- a/Source/NETworkManager/ViewModels/SettingsProfilesViewModel.cs
+++ b/Source/NETworkManager/ViewModels/SettingsProfilesViewModel.cs
@@ -22,6 +22,8 @@ public class SettingsProfilesViewModel : ViewModelBase
public Action CloseAction { get; set; }
+ private readonly bool _isLoading;
+
private string _location;
public string Location
@@ -67,12 +69,31 @@ public ProfileFileInfo SelectedProfileFile
}
}
+ private int _maximumNumberOfBackups;
+
+ public int MaximumNumberOfBackups
+ {
+ get => _maximumNumberOfBackups;
+ set
+ {
+ if (value == _maximumNumberOfBackups)
+ return;
+
+ if (!_isLoading)
+ SettingsManager.Current.Profiles_MaximumNumberOfBackups = value;
+
+ _maximumNumberOfBackups = value;
+ OnPropertyChanged();
+ }
+ }
#endregion
#region Constructor, LoadSettings
public SettingsProfilesViewModel()
{
+ _isLoading = true;
+
ProfileFiles = new CollectionViewSource { Source = ProfileManager.ProfileFiles }.View;
ProfileFiles.SortDescriptions.Add(
new SortDescription(nameof(ProfileFileInfo.Name), ListSortDirection.Ascending));
@@ -80,11 +101,14 @@ public SettingsProfilesViewModel()
SelectedProfileFile = ProfileFiles.Cast().FirstOrDefault();
LoadSettings();
+
+ _isLoading = false;
}
private void LoadSettings()
{
Location = ProfileManager.GetProfilesFolderLocation();
+ MaximumNumberOfBackups = SettingsManager.Current.Profiles_MaximumNumberOfBackups;
}
#endregion
diff --git a/Source/NETworkManager/ViewModels/SettingsSettingsViewModel.cs b/Source/NETworkManager/ViewModels/SettingsSettingsViewModel.cs
index 399ffb1e02..fbe8f3be3f 100644
--- a/Source/NETworkManager/ViewModels/SettingsSettingsViewModel.cs
+++ b/Source/NETworkManager/ViewModels/SettingsSettingsViewModel.cs
@@ -1,8 +1,6 @@
-using MahApps.Metro.SimpleChildWindow;
-using NETworkManager.Localization.Resources;
+using NETworkManager.Localization.Resources;
using NETworkManager.Settings;
using NETworkManager.Utilities;
-using NETworkManager.Views;
using System;
using System.Diagnostics;
using System.Threading.Tasks;
@@ -16,6 +14,8 @@ public class SettingsSettingsViewModel : ViewModelBase
#region Variables
public Action CloseAction { get; set; }
+ private readonly bool _isLoading;
+
private string _location;
public string Location
@@ -30,18 +30,42 @@ public string Location
OnPropertyChanged();
}
}
+
+ private int _maximumNumberOfBackups;
+
+ public int MaximumNumberOfBackups
+ {
+ get => _maximumNumberOfBackups;
+ set
+ {
+ if (value == _maximumNumberOfBackups)
+ return;
+
+ if (!_isLoading)
+ SettingsManager.Current.Settings_MaximumNumberOfBackups = value;
+
+ _maximumNumberOfBackups = value;
+ OnPropertyChanged();
+ }
+ }
+
#endregion
#region Constructor, LoadSettings
public SettingsSettingsViewModel()
{
+ _isLoading = true;
+
LoadSettings();
+
+ _isLoading = false;
}
private void LoadSettings()
{
Location = SettingsManager.GetSettingsFolderLocation();
+ MaximumNumberOfBackups = SettingsManager.Current.Settings_MaximumNumberOfBackups;
}
#endregion
diff --git a/Source/NETworkManager/Views/SettingsSettingsView.xaml b/Source/NETworkManager/Views/SettingsSettingsView.xaml
index 7f91995927..c97e846d8a 100644
--- a/Source/NETworkManager/Views/SettingsSettingsView.xaml
+++ b/Source/NETworkManager/Views/SettingsSettingsView.xaml
@@ -3,6 +3,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls"
xmlns:iconPacks="http://metro.mahapps.com/winfx/xaml/iconpacks"
xmlns:viewModels="clr-namespace:NETworkManager.ViewModels"
xmlns:localization="clr-namespace:NETworkManager.Localization.Resources;assembly=NETworkManager.Localization"
@@ -30,8 +31,23 @@
+
+
+
+
+
+
+
+
+
+
+ Style="{StaticResource DefaultButton}" HorizontalAlignment="Left"
+ />
\ No newline at end of file
From 4b0f349d3bd682513712f4b7145218bf6693472b Mon Sep 17 00:00:00 2001
From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com>
Date: Sun, 11 Jan 2026 22:32:49 +0100
Subject: [PATCH 2/4] Feature: Daily Backup
---
.../Resources/Strings.Designer.cs | 33 ++++++++-------
.../Resources/Strings.resx | 13 +++---
.../GlobalStaticConfiguration.cs | 2 +
.../NETworkManager.Settings/SettingsInfo.cs | 30 +++++++++++++
.../SettingsManager.cs | 6 +--
.../ViewModels/SettingsProfilesViewModel.cs | 19 +++++++++
.../ViewModels/SettingsSettingsViewModel.cs | 19 +++++++++
.../Views/SettingsProfilesView.xaml | 24 ++++++++++-
.../Views/SettingsSettingsView.xaml | 16 ++++---
Website/docs/changelog/next-release.md | 2 +-
Website/docs/settings/profiles.md | 42 +++++++++++++++++--
Website/docs/settings/settings.md | 32 ++++++++++++--
12 files changed, 197 insertions(+), 41 deletions(-)
diff --git a/Source/NETworkManager.Localization/Resources/Strings.Designer.cs b/Source/NETworkManager.Localization/Resources/Strings.Designer.cs
index 4e7e0aca64..b57b12bff7 100644
--- a/Source/NETworkManager.Localization/Resources/Strings.Designer.cs
+++ b/Source/NETworkManager.Localization/Resources/Strings.Designer.cs
@@ -2220,6 +2220,15 @@ public static string Country {
}
}
+ ///
+ /// Sucht eine lokalisierte Zeichenfolge, die Create daily backup ähnelt.
+ ///
+ public static string CreateDailyBackup {
+ get {
+ return ResourceManager.GetString("CreateDailyBackup", resourceCulture);
+ }
+ }
+
///
/// Sucht eine lokalisierte Zeichenfolge, die Credential ähnelt.
///
@@ -4450,6 +4459,15 @@ public static string HelpMessage_ExperimentalFeatures {
}
}
+ ///
+ /// Sucht eine lokalisierte Zeichenfolge, die Number of backups that are retained before the oldest one is deleted. ähnelt.
+ ///
+ public static string HelpMessage_MaximumNumberOfBackups {
+ get {
+ return ResourceManager.GetString("HelpMessage_MaximumNumberOfBackups", resourceCulture);
+ }
+ }
+
///
/// Sucht eine lokalisierte Zeichenfolge, die Application that is displayed at startup. ähnelt.
///
@@ -4552,19 +4570,6 @@ public static string HelpMessage_SaveCredentials {
}
}
- ///
- /// Sucht eine lokalisierte Zeichenfolge, die The number of settings backups that are retained before the oldest one is deleted.
- ///
- ///An automatic backup is only created once a day, before saving a change.
- ///
- ///The value 0 disables the creation of automatic backups. Backups that have already been created are not deleted. ähnelt.
- ///
- public static string HelpMessage_SettingsMaximumNumberOfBackups {
- get {
- return ResourceManager.GetString("HelpMessage_SettingsMaximumNumberOfBackups", resourceCulture);
- }
- }
-
///
/// Sucht eine lokalisierte Zeichenfolge, die Displays the status bar in the bottom-left of the WebView when hovering over a link. ähnelt.
///
@@ -5797,7 +5802,7 @@ public static string MaximumHops {
}
///
- /// Sucht eine lokalisierte Zeichenfolge, die Maximum Number of Backups ähnelt.
+ /// Sucht eine lokalisierte Zeichenfolge, die Maximum number of backups ähnelt.
///
public static string MaximumNumberOfBackups {
get {
diff --git a/Source/NETworkManager.Localization/Resources/Strings.resx b/Source/NETworkManager.Localization/Resources/Strings.resx
index 14ca461f78..3dd4967eaf 100644
--- a/Source/NETworkManager.Localization/Resources/Strings.resx
+++ b/Source/NETworkManager.Localization/Resources/Strings.resx
@@ -3952,13 +3952,12 @@ If you click Cancel, the profile file will remain unencrypted.
Could not parse "{0}".
- Maximum Number of Backups
+ Maximum number of backups
-
- The number of settings backups that are retained before the oldest one is deleted.
-
-An automatic backup is only created once a day, before saving a change.
-
-The value 0 disables the creation of automatic backups. Backups that have already been created are not deleted.
+
+ Create daily backup
+
+
+ Number of backups that are retained before the oldest one is deleted.
\ No newline at end of file
diff --git a/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs b/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs
index 28c92928a6..851a5c233c 100644
--- a/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs
+++ b/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs
@@ -81,9 +81,11 @@ public static class GlobalStaticConfiguration
public static bool Experimental_EnableExperimentalFeatures => false;
// Settings: Profiles
+ public static bool Profiles_IsDailyBackupEnabled => true;
public static int Profiles_MaximumNumberOfBackups => 10;
// Settings: Settings
+ public static bool Settings_IsDailyBackupEnabled => true;
public static int Settings_MaximumNumberOfBackups => 10;
// Application: Dashboard
diff --git a/Source/NETworkManager.Settings/SettingsInfo.cs b/Source/NETworkManager.Settings/SettingsInfo.cs
index b8f993cff0..97834d7515 100644
--- a/Source/NETworkManager.Settings/SettingsInfo.cs
+++ b/Source/NETworkManager.Settings/SettingsInfo.cs
@@ -582,6 +582,21 @@ public string Profiles_LastSelected
}
}
+ private bool _profiles_IsDailyBackupEnabled = GlobalStaticConfiguration.Profiles_IsDailyBackupEnabled;
+
+ public bool Profiles_IsDailyBackupEnabled
+ {
+ get => _profiles_IsDailyBackupEnabled;
+ set
+ {
+ if (value == _profiles_IsDailyBackupEnabled)
+ return;
+
+ _profiles_IsDailyBackupEnabled = value;
+ OnPropertyChanged();
+ }
+ }
+
private int _profiles_MaximumNumberOfBackups = GlobalStaticConfiguration.Profiles_MaximumNumberOfBackups;
public int Profiles_MaximumNumberOfBackups
@@ -598,6 +613,21 @@ public int Profiles_MaximumNumberOfBackups
}
// Settings
+ private bool _settings_IsDailyBackupEnabled = GlobalStaticConfiguration.Settings_IsDailyBackupEnabled;
+
+ public bool Settings_IsDailyBackupEnabled
+ {
+ get => _settings_IsDailyBackupEnabled;
+ set
+ {
+ if (value == _settings_IsDailyBackupEnabled)
+ return;
+
+ _settings_IsDailyBackupEnabled = value;
+ OnPropertyChanged();
+ }
+ }
+
private int _settings_MaximumNumberOfBackups = GlobalStaticConfiguration.Settings_MaximumNumberOfBackups;
public int Settings_MaximumNumberOfBackups
diff --git a/Source/NETworkManager.Settings/SettingsManager.cs b/Source/NETworkManager.Settings/SettingsManager.cs
index 6aeb6b9a96..a0c5c9c1ca 100644
--- a/Source/NETworkManager.Settings/SettingsManager.cs
+++ b/Source/NETworkManager.Settings/SettingsManager.cs
@@ -265,10 +265,8 @@ private static void SerializeToFile(string filePath)
/// called as part of a daily maintenance routine.
private static void CreateDailyBackupIfNeeded()
{
- var maxBackups = Current.Settings_MaximumNumberOfBackups;
-
// Check if backups are disabled
- if (maxBackups == 0)
+ if (!Current.Settings_IsDailyBackupEnabled)
{
Log.Debug("Daily backups are disabled. Skipping backup creation...");
@@ -294,7 +292,7 @@ private static void CreateDailyBackupIfNeeded()
// Cleanup old backups
CleanupBackups(GetSettingsBackupFolderLocation(),
GetSettingsFileName(),
- maxBackups);
+ Current.Settings_MaximumNumberOfBackups);
Current.LastBackup = currentDate;
}
diff --git a/Source/NETworkManager/ViewModels/SettingsProfilesViewModel.cs b/Source/NETworkManager/ViewModels/SettingsProfilesViewModel.cs
index 04f4b7fcb1..7dd0e17546 100644
--- a/Source/NETworkManager/ViewModels/SettingsProfilesViewModel.cs
+++ b/Source/NETworkManager/ViewModels/SettingsProfilesViewModel.cs
@@ -69,6 +69,24 @@ public ProfileFileInfo SelectedProfileFile
}
}
+ private bool _isDailyBackupEnabled;
+
+ public bool IsDailyBackupEnabled
+ {
+ get => _isDailyBackupEnabled;
+ set
+ {
+ if (value == _isDailyBackupEnabled)
+ return;
+
+ if (!_isLoading)
+ SettingsManager.Current.Profiles_IsDailyBackupEnabled = value;
+
+ _isDailyBackupEnabled = value;
+ OnPropertyChanged();
+ }
+ }
+
private int _maximumNumberOfBackups;
public int MaximumNumberOfBackups
@@ -108,6 +126,7 @@ public SettingsProfilesViewModel()
private void LoadSettings()
{
Location = ProfileManager.GetProfilesFolderLocation();
+ IsDailyBackupEnabled = SettingsManager.Current.Profiles_IsDailyBackupEnabled;
MaximumNumberOfBackups = SettingsManager.Current.Profiles_MaximumNumberOfBackups;
}
diff --git a/Source/NETworkManager/ViewModels/SettingsSettingsViewModel.cs b/Source/NETworkManager/ViewModels/SettingsSettingsViewModel.cs
index fbe8f3be3f..dc1d7d95b1 100644
--- a/Source/NETworkManager/ViewModels/SettingsSettingsViewModel.cs
+++ b/Source/NETworkManager/ViewModels/SettingsSettingsViewModel.cs
@@ -31,6 +31,24 @@ public string Location
}
}
+ private bool _isDailyBackupEnabled;
+
+ public bool IsDailyBackupEnabled
+ {
+ get => _isDailyBackupEnabled;
+ set
+ {
+ if (value == _isDailyBackupEnabled)
+ return;
+
+ if (!_isLoading)
+ SettingsManager.Current.Settings_IsDailyBackupEnabled = value;
+
+ _isDailyBackupEnabled = value;
+ OnPropertyChanged();
+ }
+ }
+
private int _maximumNumberOfBackups;
public int MaximumNumberOfBackups
@@ -65,6 +83,7 @@ public SettingsSettingsViewModel()
private void LoadSettings()
{
Location = SettingsManager.GetSettingsFolderLocation();
+ IsDailyBackupEnabled = SettingsManager.Current.Settings_IsDailyBackupEnabled;
MaximumNumberOfBackups = SettingsManager.Current.Settings_MaximumNumberOfBackups;
}
diff --git a/Source/NETworkManager/Views/SettingsProfilesView.xaml b/Source/NETworkManager/Views/SettingsProfilesView.xaml
index d73f2640c8..0989c4a24d 100644
--- a/Source/NETworkManager/Views/SettingsProfilesView.xaml
+++ b/Source/NETworkManager/Views/SettingsProfilesView.xaml
@@ -3,6 +3,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls"
xmlns:converters="clr-namespace:NETworkManager.Converters;assembly=NETworkManager.Converters"
xmlns:iconPacks="http://metro.mahapps.com/winfx/xaml/iconpacks"
xmlns:wpfHelpers="clr-namespace:NETworkManager.Utilities.WPF;assembly=NETworkManager.Utilities.WPF"
@@ -144,7 +145,8 @@
+
+
+
+
+
+
+
+
+
+
+
-
\ No newline at end of file
+
diff --git a/Source/NETworkManager/Views/SettingsSettingsView.xaml b/Source/NETworkManager/Views/SettingsSettingsView.xaml
index c97e846d8a..c4880b397e 100644
--- a/Source/NETworkManager/Views/SettingsSettingsView.xaml
+++ b/Source/NETworkManager/Views/SettingsSettingsView.xaml
@@ -32,13 +32,17 @@
-
-
-
+
+
+
+
diff --git a/Website/docs/changelog/next-release.md b/Website/docs/changelog/next-release.md
index 28bae845be..bf65f5504a 100644
--- a/Website/docs/changelog/next-release.md
+++ b/Website/docs/changelog/next-release.md
@@ -65,7 +65,7 @@ Release date: **xx.xx.2025**
**Settings**
- Settings file format migrated from `XML` to `JSON`. The settings file will be automatically converted on first load 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)
+- Create a daily backup of the settings file before saving changes. By default, up to `10` backup files are kept in the `Backups` subfolder of the settings directory. [#3283](https://github.com/BornToBeRoot/NETworkManager/pull/3283) [#3302](https://github.com/BornToBeRoot/NETworkManager/pull/3302)
**Dashboard**
diff --git a/Website/docs/settings/profiles.md b/Website/docs/settings/profiles.md
index 5789f625fb..186924aa4b 100644
--- a/Website/docs/settings/profiles.md
+++ b/Website/docs/settings/profiles.md
@@ -19,9 +19,21 @@ Folder where the application profiles are stored.
:::note
-It is recommended to backup the above files on a regular basis.
-
-To restore the profiles, close the application and copy the files from the backup to the above location.
+**Recommendation**
+It is strongly recommended to regularly back up your profile files.
+
+**Automatic backups**
+NETworkManager automatically creates a backup of the profile file before applying any changes. See [Create daily backup](#create-daily-backup) and [Maximum number of backups](#maximum-number-of-backups) for configuration options.
+- Location: `Profiles\Backups` subfolder (relative to the main configuration directory)
+- Naming: timestamped (e.g. `yyyyMMddHHmmss_.json`)
+- Frequency: **once per day** at most (even if multiple changes occur)
+- Retention: keeps the **10 most recent backups** (default)
+
+**Restoring profiles**
+1. Completely close NETworkManager
+2. Locate the desired backup in `Profiles\Backups`
+3. Copy the file(s) back to the original folder (overwrite existing files or copy them under a different name)
+4. Restart the application
:::
@@ -44,3 +56,27 @@ Profile files can be encrypted with a master password. See [FAQ > How to enable
At least one profile is required and must exist.
:::
+
+### Create daily backup
+
+Create a daily backup of the profile file before applying any changes.
+
+**Type**: `Boolean`
+
+**Default:** `Enabled`
+
+:::note
+
+Backups are stored in the `Profiles\Backups` subfolder. See [Location](#location) for more details.
+
+Backups are created at most once per day, even if multiple changes occur.
+
+:::
+
+### Maximum number of backups
+
+Maximum number of backups to keep. Older backups will be deleted automatically once a new backup is created.
+
+**Type:** `Integer` [Min `1`, Max `365`]
+
+**Default:** `10`
diff --git a/Website/docs/settings/settings.md b/Website/docs/settings/settings.md
index f772085232..32342b3d6e 100644
--- a/Website/docs/settings/settings.md
+++ b/Website/docs/settings/settings.md
@@ -23,20 +23,44 @@ Folder where the application settings are stored.
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)
+NETworkManager automatically creates a backup of the settings files before applying any changes. See [Create daily backup](#create-daily-backup) and [Maximum number of backups](#maximum-number-of-backups) for configuration options.
+- Location: `Settings\Backups` subfolder
- 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**
+- Retention: keeps the **10 most recent backups** (default)
**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)
+3. Copy the file(s) back to the original folder (overwriting existing files)
4. Restart the application
:::
+### Create daily backup
+
+Create a daily backup of the application settings before applying any changes.
+
+**Type**: `Boolean`
+
+**Default:** `Enabled`
+
+:::note
+
+Backups are stored in the `Settings\Backups` subfolder. See [Location](#location) for more details.
+
+Backups are created at most once per day, even if multiple changes occur.
+
+:::
+
+### Maximum number of backups
+
+Maximum number of backups to keep. Older backups will be deleted automatically once a new backup is created.
+
+**Type:** `Integer` [Min `1`, Max `365`]
+
+**Default:** `10`
+
### Reset
Button to reset all application settings to their default values.
From 88928b298669af8a25a30c0b0666c186d78c8787 Mon Sep 17 00:00:00 2001
From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com>
Date: Sun, 11 Jan 2026 23:01:01 +0100
Subject: [PATCH 3/4] Feature: Add more backups before important operations
---
.../NETworkManager.Profiles/ProfileManager.cs | 60 +++++++++++++------
1 file changed, 42 insertions(+), 18 deletions(-)
diff --git a/Source/NETworkManager.Profiles/ProfileManager.cs b/Source/NETworkManager.Profiles/ProfileManager.cs
index e7cdb73390..c236eab1ac 100644
--- a/Source/NETworkManager.Profiles/ProfileManager.cs
+++ b/Source/NETworkManager.Profiles/ProfileManager.cs
@@ -25,14 +25,14 @@ public static class ProfileManager
private const string ProfilesFolderName = "Profiles";
///
- /// Default profile name.
+ /// Profiles backups directory name.
///
- private const string ProfilesDefaultFileName = "Default";
+ private static string BackupFolderName => "Backups";
///
- /// Profiles backups directory name.
+ /// Default profile name.
///
- private static string BackupFolderName => "Backups";
+ private const string ProfilesDefaultFileName = "Default";
///
/// Profile file extension.
@@ -290,14 +290,21 @@ public static void CreateEmptyProfileFile(string profileName)
/// New of the profile file.
public static void RenameProfileFile(ProfileFileInfo profileFileInfo, string newProfileName)
{
+ // Check if the profile is currently in use
var switchProfile = false;
-
+
if (LoadedProfileFile != null && LoadedProfileFile.Equals(profileFileInfo))
{
Save();
switchProfile = true;
}
+ // Create backup
+ Backup(profileFileInfo.Path,
+ GetProfilesBackupFolderLocation(),
+ TimestampHelper.GetTimestampFilename(Path.GetFileName(profileFileInfo.Path)));
+
+ // Create new profile info with the new name
ProfileFileInfo newProfileFileInfo = new(newProfileName,
Path.Combine(GetProfilesFolderLocation(), $"{newProfileName}{Path.GetExtension(profileFileInfo.Path)}"),
profileFileInfo.IsEncrypted)
@@ -306,15 +313,18 @@ public static void RenameProfileFile(ProfileFileInfo profileFileInfo, string new
IsPasswordValid = profileFileInfo.IsPasswordValid
};
+ // Copy the profile file to the new location
File.Copy(profileFileInfo.Path, newProfileFileInfo.Path);
ProfileFiles.Add(newProfileFileInfo);
+ // Switch profile, if it was previously loaded
if (switchProfile)
{
Switch(newProfileFileInfo, false);
LoadedProfileFileChanged(LoadedProfileFile, true);
}
+ // Remove the old profile file
File.Delete(profileFileInfo.Path);
ProfileFiles.Remove(profileFileInfo);
}
@@ -353,6 +363,11 @@ public static void EnableEncryption(ProfileFileInfo profileFileInfo, SecureStrin
switchProfile = true;
}
+ // Create backup
+ Backup(profileFileInfo.Path,
+ GetProfilesBackupFolderLocation(),
+ TimestampHelper.GetTimestampFilename(Path.GetFileName(profileFileInfo.Path)));
+
// Create a new profile info with the encryption infos
var newProfileFileInfo = new ProfileFileInfo(profileFileInfo.Name,
Path.ChangeExtension(profileFileInfo.Path, ProfileFileExtensionEncrypted), true)
@@ -361,9 +376,9 @@ public static void EnableEncryption(ProfileFileInfo profileFileInfo, SecureStrin
IsPasswordValid = true
};
- List profiles = Path.GetExtension(profileFileInfo.Path) == LegacyProfileFileExtension
- ? DeserializeFromXmlFile(profileFileInfo.Path)
- : DeserializeFromFile(profileFileInfo.Path);
+ List profiles = Path.GetExtension(profileFileInfo.Path) == LegacyProfileFileExtension ?
+ DeserializeFromXmlFile(profileFileInfo.Path) :
+ DeserializeFromFile(profileFileInfo.Path);
// Save the encrypted file
var decryptedBytes = SerializeToByteArray(profiles);
@@ -409,7 +424,12 @@ public static void ChangeMasterPassword(ProfileFileInfo profileFileInfo, SecureS
switchProfile = true;
}
- // Create a new profile info with the encryption infos
+ // Create backup
+ Backup(profileFileInfo.Path,
+ GetProfilesBackupFolderLocation(),
+ TimestampHelper.GetTimestampFilename(Path.GetFileName(profileFileInfo.Path)));
+
+ // Create new profile info with the encryption infos
var newProfileFileInfo = new ProfileFileInfo(profileFileInfo.Name,
Path.ChangeExtension(profileFileInfo.Path, ProfileFileExtensionEncrypted), true)
{
@@ -423,11 +443,9 @@ public static void ChangeMasterPassword(ProfileFileInfo profileFileInfo, SecureS
GlobalStaticConfiguration.Profile_EncryptionKeySize,
GlobalStaticConfiguration.Profile_EncryptionIterations);
- List profiles;
-
- profiles = IsXmlContent(decryptedBytes)
- ? DeserializeFromXmlByteArray(decryptedBytes)
- : DeserializeFromByteArray(decryptedBytes);
+ List profiles = IsXmlContent(decryptedBytes) ?
+ DeserializeFromXmlByteArray(decryptedBytes) :
+ DeserializeFromByteArray(decryptedBytes);
// Save the encrypted file
decryptedBytes = SerializeToByteArray(profiles);
@@ -468,7 +486,12 @@ public static void DisableEncryption(ProfileFileInfo profileFileInfo, SecureStri
switchProfile = true;
}
- // Create a new profile info
+ // Create backup
+ Backup(profileFileInfo.Path,
+ GetProfilesBackupFolderLocation(),
+ TimestampHelper.GetTimestampFilename(Path.GetFileName(profileFileInfo.Path)));
+
+ // Create new profile info
var newProfileFileInfo = new ProfileFileInfo(profileFileInfo.Name,
Path.ChangeExtension(profileFileInfo.Path, ProfileFileExtension));
@@ -478,9 +501,10 @@ public static void DisableEncryption(ProfileFileInfo profileFileInfo, SecureStri
GlobalStaticConfiguration.Profile_EncryptionKeySize,
GlobalStaticConfiguration.Profile_EncryptionIterations);
- List profiles = IsXmlContent(decryptedBytes)
- ? DeserializeFromXmlByteArray(decryptedBytes)
- : DeserializeFromByteArray(decryptedBytes);
+ List profiles = IsXmlContent(decryptedBytes) ?
+ DeserializeFromXmlByteArray(decryptedBytes) :
+ DeserializeFromByteArray(decryptedBytes);
+
// Save the decrypted profiles to the profile file
SerializeToFile(newProfileFileInfo.Path, profiles);
From 9b4ed37ce8eb6f41b365f6fc11ae07f7116971f4 Mon Sep 17 00:00:00 2001
From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com>
Date: Tue, 27 Jan 2026 01:47:39 +0100
Subject: [PATCH 4/4] Feature: Create daily backup
---
Source/NETworkManager.Profiles/GroupInfo.cs | 2 +-
.../GroupInfoSerializable.cs | 1 +
.../ProfileFileData.cs | 82 ++++++++
.../NETworkManager.Profiles/ProfileManager.cs | 190 ++++++++++++++++--
.../NETworkManager.Settings/SettingsInfo.cs | 2 +-
.../SettingsManager.cs | 47 +++--
.../TimestampHelper.cs | 2 +-
Source/NETworkManager/App.xaml.cs | 15 +-
8 files changed, 299 insertions(+), 42 deletions(-)
create mode 100644 Source/NETworkManager.Profiles/ProfileFileData.cs
diff --git a/Source/NETworkManager.Profiles/GroupInfo.cs b/Source/NETworkManager.Profiles/GroupInfo.cs
index 4b39c78043..2598a48d05 100644
--- a/Source/NETworkManager.Profiles/GroupInfo.cs
+++ b/Source/NETworkManager.Profiles/GroupInfo.cs
@@ -311,4 +311,4 @@ public GroupInfo(GroupInfo group) : this(group.Name)
[XmlIgnore] public SecureString SNMP_Auth { get; set; }
public SNMPV3PrivacyProvider SNMP_PrivacyProvider { get; set; } = GlobalStaticConfiguration.SNMP_PrivacyProvider;
[XmlIgnore] public SecureString SNMP_Priv { get; set; }
-}
\ No newline at end of file
+}
diff --git a/Source/NETworkManager.Profiles/GroupInfoSerializable.cs b/Source/NETworkManager.Profiles/GroupInfoSerializable.cs
index 2ef946500c..01e9a03aa1 100644
--- a/Source/NETworkManager.Profiles/GroupInfoSerializable.cs
+++ b/Source/NETworkManager.Profiles/GroupInfoSerializable.cs
@@ -10,6 +10,7 @@ public GroupInfoSerializable()
public GroupInfoSerializable(GroupInfo profileGroup) : base(profileGroup)
{
+
}
///
diff --git a/Source/NETworkManager.Profiles/ProfileFileData.cs b/Source/NETworkManager.Profiles/ProfileFileData.cs
new file mode 100644
index 0000000000..db2093e901
--- /dev/null
+++ b/Source/NETworkManager.Profiles/ProfileFileData.cs
@@ -0,0 +1,82 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+using System.Text.Json.Serialization;
+
+namespace NETworkManager.Profiles;
+
+///
+/// Represents the data structure for a profile file, including versioning, backup information, and groups of profiles.
+///
+/// This class supports property change notification through the
+/// interface, allowing consumers to track changes to its properties. The property
+/// indicates whether the data has been modified since it was last saved, but is not persisted when serializing the
+/// object. Use this class to manage and persist user profile data, including handling schema migrations via the property.
+public class ProfileFileData : INotifyPropertyChanged
+{
+ ///
+ /// Occurs when a property value changes.
+ ///
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ ///
+ /// Helper method to raise the event and mark data as changed.
+ ///
+ /// Name of the property that changed.
+ private void OnPropertyChanged([CallerMemberName] string propertyName = null)
+ {
+ ProfilesChanged = true;
+
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+
+ ///
+ /// Indicates if the profile file data has been modified and needs to be saved.
+ /// This is not serialized to the file.
+ ///
+ [JsonIgnore]
+ public bool ProfilesChanged { get; set; }
+
+ private int _version = 1;
+
+ ///
+ /// Schema version for handling future migrations.
+ ///
+ public int Version
+ {
+ get => _version;
+ set
+ {
+ if (value == _version)
+ return;
+
+ _version = value;
+ OnPropertyChanged();
+ }
+ }
+
+ private DateTime? _lastBackup;
+
+ ///
+ /// Date of the last backup (used for daily backup tracking).
+ ///
+ public DateTime? LastBackup
+ {
+ get => _lastBackup;
+ set
+ {
+ if (value == _lastBackup)
+ return;
+
+ _lastBackup = value;
+ OnPropertyChanged();
+ }
+ }
+
+ ///
+ /// List of groups containing profiles.
+ ///
+ public List Groups { get; set; } = [];
+}
diff --git a/Source/NETworkManager.Profiles/ProfileManager.cs b/Source/NETworkManager.Profiles/ProfileManager.cs
index c236eab1ac..e540c02369 100644
--- a/Source/NETworkManager.Profiles/ProfileManager.cs
+++ b/Source/NETworkManager.Profiles/ProfileManager.cs
@@ -92,14 +92,31 @@ private set
}
///
- /// Currently loaded groups with profiles.
+ /// Currently loaded profile file data (wrapper containing groups and metadata).
+ /// This is updated during load/save operations.
///
- public static List Groups { get; set; } = [];
+ private static ProfileFileData _loadedProfileFileData = new();
///
- /// Indicates if profiles have changed.
+ /// Currently loaded profile file data (wrapper containing groups and metadata).
+ /// This is updated during load/save operations.
///
- public static bool ProfilesChanged { get; set; }
+ public static ProfileFileData LoadedProfileFileData
+ {
+ get => _loadedProfileFileData;
+ private set
+ {
+ if (Equals(value, _loadedProfileFileData))
+ return;
+
+ _loadedProfileFileData = value;
+ }
+ }
+
+ ///
+ /// Currently loaded groups with profiles (working copy in memory).
+ ///
+ public static List Groups { get; set; } = [];
#endregion
@@ -172,9 +189,9 @@ private static void ProfileMigrationCompleted()
///
/// Method to fire the .
///
- private static void ProfilesUpdated()
+ private static void ProfilesUpdated(bool profilesChanged = true)
{
- ProfilesChanged = true;
+ LoadedProfileFileData?.ProfilesChanged = profilesChanged;
OnProfilesUpdated?.Invoke(null, EventArgs.Empty);
}
@@ -292,7 +309,7 @@ public static void RenameProfileFile(ProfileFileInfo profileFileInfo, string new
{
// Check if the profile is currently in use
var switchProfile = false;
-
+
if (LoadedProfileFile != null && LoadedProfileFile.Equals(profileFileInfo))
{
Save();
@@ -582,7 +599,7 @@ private static void Load(ProfileFileInfo profileFileInfo)
groups = DeserializeFromByteArray(decryptedBytes);
}
- AddGroups(groups);
+ AddGroups(groups, false);
// Password is valid
ProfileFiles.FirstOrDefault(x => x.Equals(profileFileInfo))!.IsPasswordValid = true;
@@ -604,8 +621,6 @@ private static void Load(ProfileFileInfo profileFileInfo)
// Load from legacy XML file
groups = DeserializeFromXmlFile(profileFileInfo.Path);
- ProfilesChanged = false;
-
LoadedProfileFile = profileFileInfo;
// Create a backup of the legacy XML file and delete the original
@@ -646,7 +661,7 @@ private static void Load(ProfileFileInfo profileFileInfo)
groups = DeserializeFromFile(profileFileInfo.Path);
}
- AddGroups(groups);
+ AddGroups(groups, false);
}
}
else
@@ -656,8 +671,6 @@ private static void Load(ProfileFileInfo profileFileInfo)
throw new FileNotFoundException($"{profileFileInfo.Path} could not be found!");
}
- ProfilesChanged = false;
-
LoadedProfileFile = profileFileInfo;
if (loadedProfileUpdated)
@@ -671,7 +684,7 @@ 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.");
+ Log.Warn("Cannot save profiles because no profile file is loaded or the profile file is encrypted and not yet unlocked.");
return;
}
@@ -679,6 +692,9 @@ public static void Save()
// Ensure the profiles directory exists.
Directory.CreateDirectory(GetProfilesFolderLocation());
+ // Create backup before modifying
+ CreateDailyBackupIfNeeded();
+
// Write profiles to the profile file (JSON, optionally encrypted).
if (LoadedProfileFile.IsEncrypted)
{
@@ -699,7 +715,7 @@ public static void Save()
SerializeToFile(LoadedProfileFile.Path, [.. Groups]);
}
- ProfilesChanged = false;
+ LoadedProfileFileData?.ProfilesChanged = false;
}
///
@@ -708,10 +724,11 @@ public static void Save()
/// Save loaded profile file (default is true)
public static void Unload(bool saveLoadedProfiles = true)
{
- if (saveLoadedProfiles && LoadedProfileFile != null && ProfilesChanged)
+ if (saveLoadedProfiles && LoadedProfileFile != null && LoadedProfileFileData?.ProfilesChanged == true)
Save();
LoadedProfileFile = null;
+ LoadedProfileFileData = null;
Groups.Clear();
@@ -741,7 +758,13 @@ public static void Switch(ProfileFileInfo info, bool saveLoadedProfiles = true)
/// List of the groups as to serialize.
private static void SerializeToFile(string filePath, List groups)
{
- var jsonString = JsonSerializer.Serialize(SerializeGroup(groups), JsonOptions);
+ // Ensure LoadedProfileFileData exists
+ LoadedProfileFileData ??= new ProfileFileData();
+
+ // Update LoadedProfileFileData with current groups
+ LoadedProfileFileData.Groups = SerializeGroup(groups);
+
+ var jsonString = JsonSerializer.Serialize(LoadedProfileFileData, JsonOptions);
File.WriteAllText(filePath, jsonString);
}
@@ -753,7 +776,13 @@ private static void SerializeToFile(string filePath, List groups)
/// Serialized list of groups as as byte array.
private static byte[] SerializeToByteArray(List groups)
{
- var jsonString = JsonSerializer.Serialize(SerializeGroup(groups), JsonOptions);
+ // Ensure LoadedProfileFileData exists
+ LoadedProfileFileData ??= new ProfileFileData();
+
+ // Update LoadedProfileFileData with current groups
+ LoadedProfileFileData.Groups = SerializeGroup(groups);
+
+ var jsonString = JsonSerializer.Serialize(LoadedProfileFileData, JsonOptions);
return Encoding.UTF8.GetBytes(jsonString);
}
@@ -873,11 +902,36 @@ private static List DeserializeFromXmlByteArray(byte[] xml)
/// List of groups as .
private static List DeserializeFromJson(string jsonString)
{
+ try
+ {
+ var profileFileData = JsonSerializer.Deserialize(jsonString, JsonOptions);
+
+ if (profileFileData?.Groups != null)
+ {
+ LoadedProfileFileData = profileFileData;
+
+ return DeserializeGroup(profileFileData.Groups);
+ }
+ }
+ catch (JsonException)
+ {
+ Log.Info("Failed to deserialize as ProfileFileData, trying legacy format (direct Groups array)...");
+ }
+
+ // Fallback: Try to deserialize as legacy format (direct array of GroupInfoSerializable)
var groupsSerializable = JsonSerializer.Deserialize>(jsonString, JsonOptions);
if (groupsSerializable == null)
throw new InvalidOperationException("Failed to deserialize JSON profile file.");
+ // Create ProfileFileData wrapper for legacy format
+ LoadedProfileFileData = new ProfileFileData
+ {
+ Groups = groupsSerializable
+ };
+
+ Log.Info("Successfully loaded profile file in legacy format. It will be migrated to new format on next save.");
+
return DeserializeGroup(groupsSerializable);
}
@@ -997,23 +1051,24 @@ private static List DeserializeGroup(List grou
/// Method to add a list of to the list.
///
/// List of groups as to add.
- private static void AddGroups(List groups)
+ private static void AddGroups(List groups, bool profilesChanged = true)
{
foreach (var group in groups)
Groups.Add(group);
- ProfilesUpdated();
+ ProfilesUpdated(profilesChanged);
}
///
/// Method to add a to the list.
///
/// Group as to add.
- public static void AddGroup(GroupInfo group)
+ public static void AddGroup(GroupInfo group, bool profilesChanged = true)
+
{
Groups.Add(group);
- ProfilesUpdated();
+ ProfilesUpdated(profilesChanged);
}
///
@@ -1143,6 +1198,97 @@ public static void RemoveProfiles(IEnumerable profiles)
#region Backup
+ ///
+ /// Creates a backup of the currently loaded profile file if a backup has not already been created for the current day.
+ ///
+ private static void CreateDailyBackupIfNeeded()
+ {
+ // Skip if daily backups are disabled
+ if (!SettingsManager.Current.Profiles_IsDailyBackupEnabled)
+ {
+ Log.Info("Daily profile backups are disabled. Skipping backup creation...");
+ return;
+ }
+
+ // Skip if no profile is loaded
+ if (LoadedProfileFile == null || LoadedProfileFileData == null)
+ {
+ Log.Info("No profile file is currently loaded. Skipping backup creation...");
+ return;
+ }
+
+ // Skip if the profile file doesn't exist yet
+ if (!File.Exists(LoadedProfileFile.Path))
+ {
+ Log.Warn($"Profile file does not exist yet: {LoadedProfileFile.Path}. Skipping backup creation...");
+ return;
+ }
+
+ // Create backup if needed
+ var currentDate = DateTime.Now.Date;
+ var lastBackupDate = LoadedProfileFileData.LastBackup?.Date ?? DateTime.MinValue;
+ var profileFileName = Path.GetFileName(LoadedProfileFile.Path);
+
+ if (lastBackupDate < currentDate)
+ {
+ Log.Info($"Creating daily backup for profile: {profileFileName}");
+
+ // Create backup
+ Backup(LoadedProfileFile.Path,
+ GetProfilesBackupFolderLocation(),
+ TimestampHelper.GetTimestampFilename(profileFileName));
+
+ // Cleanup old backups
+ CleanupBackups(GetProfilesBackupFolderLocation(),
+ profileFileName,
+ SettingsManager.Current.Profiles_MaximumNumberOfBackups);
+
+ LoadedProfileFileData.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.
+ ///
+ /// The full path to the directory containing the backup files to be managed.
+ /// The profile 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 profileFileName, int maxBackupFiles)
+ {
+ // Extract profile name without extension to match all backup files regardless of extension
+ // (e.g., "Default" matches "2025-01-19_Default.json", "2025-01-19_Default.encrypted", etc.)
+ var profileNameWithoutExtension = Path.GetFileNameWithoutExtension(profileFileName);
+
+ // Get all backup files for this specific profile (any extension) sorted by timestamp (newest first)
+ var backupFiles = Directory.GetFiles(backupFolderPath)
+ .Where(f =>
+ {
+ var fileName = Path.GetFileName(f);
+
+ // Check if it's a timestamped backup and contains the profile name
+ return TimestampHelper.IsTimestampedFilename(fileName) &&
+ fileName.Contains($"_{profileNameWithoutExtension}.");
+ })
+ .OrderByDescending(f => TimestampHelper.ExtractTimestampFromFilename(Path.GetFileName(f)))
+ .ToList();
+
+ if (backupFiles.Count > maxBackupFiles)
+ Log.Info($"Cleaning up old backup files for {profileNameWithoutExtension}... 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 profile file in the given backup folder with the provided backup file name.
///
diff --git a/Source/NETworkManager.Settings/SettingsInfo.cs b/Source/NETworkManager.Settings/SettingsInfo.cs
index 97834d7515..2d96124243 100644
--- a/Source/NETworkManager.Settings/SettingsInfo.cs
+++ b/Source/NETworkManager.Settings/SettingsInfo.cs
@@ -51,7 +51,7 @@ private void OnPropertyChanged([CallerMemberName] string propertyName = null)
///
/// Determines if the welcome dialog should be shown on application start.
- ///
+ /// S
public bool WelcomeDialog_Show
{
get => _welcomeDialog_Show;
diff --git a/Source/NETworkManager.Settings/SettingsManager.cs b/Source/NETworkManager.Settings/SettingsManager.cs
index a0c5c9c1ca..34ff00a850 100644
--- a/Source/NETworkManager.Settings/SettingsManager.cs
+++ b/Source/NETworkManager.Settings/SettingsManager.cs
@@ -228,7 +228,7 @@ private static SettingsInfo DeserializeFromXmlFile(string filePath)
/// Method to save the currently loaded settings (to a file).
///
public static void Save()
- {
+ {
// Create the directory if it does not exist
Directory.CreateDirectory(GetSettingsFolderLocation());
@@ -265,24 +265,27 @@ private static void SerializeToFile(string filePath)
/// called as part of a daily maintenance routine.
private static void CreateDailyBackupIfNeeded()
{
- // Check if backups are disabled
+ // Skip if daily backups are disabled
if (!Current.Settings_IsDailyBackupEnabled)
{
- Log.Debug("Daily backups are disabled. Skipping backup creation...");
+ Log.Info("Daily backups are disabled. Skipping backup creation...");
+ return;
+ }
+ // Skip if settings file doesn't exist yet
+ if (!File.Exists(GetSettingsFilePath()))
+ {
+ Log.Warn("Settings file does not exist yet. Skipping backup creation...");
return;
}
+ // Create backup if needed
var currentDate = DateTime.Now.Date;
-
- if (Current.LastBackup < currentDate)
+ var lastBackupDate = Current.LastBackup.Date;
+
+ if (lastBackupDate < currentDate)
{
- // Check if settings file exists
- if (!File.Exists(GetSettingsFilePath()))
- {
- Log.Warn("Settings file does not exist yet. Skipping backup creation...");
- return;
- }
+ Log.Info("Creating daily backup of settings...");
// Create backup
Backup(GetSettingsFilePath(),
@@ -310,14 +313,25 @@ private static void CreateDailyBackupIfNeeded()
/// 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)
+ // Extract settings name without extension to match all backup files regardless of extension
+ // (e.g., "Settings" matches "2025-01-19_Settings.json", "2025-01-19_Settings.xml")
+ var settingsNameWithoutExtension = Path.GetFileNameWithoutExtension(settingsFileName);
+
+ // Get all backup files for settings (any extension) sorted by timestamp (newest first)
var backupFiles = Directory.GetFiles(backupFolderPath)
- .Where(f => (f.EndsWith(settingsFileName) || f.EndsWith(GetLegacySettingsFileName())) && TimestampHelper.IsTimestampedFilename(Path.GetFileName(f)))
+ .Where(f =>
+ {
+ var fileName = Path.GetFileName(f);
+
+ // Check if it's a timestamped backup and contains the settings name
+ return TimestampHelper.IsTimestampedFilename(fileName) &&
+ fileName.Contains($"_{settingsNameWithoutExtension}.");
+ })
.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}.");
+ Log.Info($"Cleaning up old backup files for {settingsNameWithoutExtension}... Found {backupFiles.Count} backups, keeping the most recent {maxBackupFiles}.");
// Delete oldest backups until the maximum number is reached
while (backupFiles.Count > maxBackupFiles)
@@ -365,6 +379,11 @@ public static void Upgrade(Version fromVersion, Version toVersion)
{
Log.Info($"Start settings upgrade from {fromVersion} to {toVersion}...");
+ // Create backup
+ Backup(GetSettingsFilePath(),
+ GetSettingsBackupFolderLocation(),
+ TimestampHelper.GetTimestampFilename(GetSettingsFileName()));
+
// 2023.3.7.0
if (fromVersion < new Version(2023, 3, 7, 0))
UpgradeTo_2023_3_7_0();
diff --git a/Source/NETworkManager.Utilities/TimestampHelper.cs b/Source/NETworkManager.Utilities/TimestampHelper.cs
index 0ed4b5b83b..6ee5e83c09 100644
--- a/Source/NETworkManager.Utilities/TimestampHelper.cs
+++ b/Source/NETworkManager.Utilities/TimestampHelper.cs
@@ -35,7 +35,7 @@ public static bool IsTimestampedFilename(string fileName)
if (fileName.Length < 16)
return false;
- var timestampString = fileName.Substring(0, 14);
+ var timestampString = fileName[..14];
return DateTime.TryParseExact(timestampString, "yyyyMMddHHmmss", CultureInfo.InvariantCulture, DateTimeStyles.None, out _);
}
diff --git a/Source/NETworkManager/App.xaml.cs b/Source/NETworkManager/App.xaml.cs
index b3e4cd3f1f..ede9bc5e36 100644
--- a/Source/NETworkManager/App.xaml.cs
+++ b/Source/NETworkManager/App.xaml.cs
@@ -271,16 +271,25 @@ private void Application_Exit(object sender, ExitEventArgs e)
private void Save()
{
+ // Save settings if they have changed
if (SettingsManager.Current.SettingsChanged)
{
Log.Info("Save application settings...");
SettingsManager.Save();
}
- if (ProfileManager.ProfilesChanged)
+ // Save profiles if they have changed
+ if (ProfileManager.LoadedProfileFile != null && ProfileManager.LoadedProfileFileData != null)
{
- Log.Info("Save current profiles...");
- ProfileManager.Save();
+ if (ProfileManager.LoadedProfileFileData.ProfilesChanged)
+ {
+ Log.Info($"Save current profile file \"{ProfileManager.LoadedProfileFile.Name}\"...");
+ ProfileManager.Save();
+ }
+ }
+ else
+ {
+ Log.Warn("Cannot save profiles because no profile file is loaded or the profile file is encrypted and not yet unlocked.");
}
}
}