Skip to content

Commit 561deb8

Browse files
BornToBeRootCopilotCopilot
authored
Feature: Automatic Profile & Settings backup (#3302)
* Feature: Configure backup retention * Feature: Daily Backup * Feature: Add more backups before important operations * Feature: Create daily backup * Feature: Refactor ProfileManager to add properties to a ProfileFile * Docs: #3302 * Update Source/NETworkManager.Settings/SettingsInfo.cs Co-authored-by: Copilot <[email protected]> * Fix: Wrong behaviour if null * Fix XAML formatting: consolidate Button closing tag to single line (#3308) * Initial plan * Fix: Consolidate Button closing tag to single line Co-authored-by: BornToBeRoot <[email protected]> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: BornToBeRoot <[email protected]> * Remove trailing whitespace from SettingsSettingsView.xaml (#3307) * Initial plan * Remove trailing whitespace from SettingsSettingsView.xaml line 40 Co-authored-by: BornToBeRoot <[email protected]> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: BornToBeRoot <[email protected]> --------- Co-authored-by: Copilot <[email protected]> Co-authored-by: Copilot <[email protected]>
1 parent 5014659 commit 561deb8

File tree

121 files changed

+1142
-432
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

121 files changed

+1142
-432
lines changed

Source/NETworkManager.Localization/Resources/Strings.Designer.cs

Lines changed: 27 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Source/NETworkManager.Localization/Resources/Strings.resx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3951,4 +3951,13 @@ If you click Cancel, the profile file will remain unencrypted.</value>
39513951
<data name="CouldNotParseX" xml:space="preserve">
39523952
<value>Could not parse "{0}".</value>
39533953
</data>
3954+
<data name="MaximumNumberOfBackups" xml:space="preserve">
3955+
<value>Maximum number of backups</value>
3956+
</data>
3957+
<data name="CreateDailyBackup" xml:space="preserve">
3958+
<value>Create daily backup</value>
3959+
</data>
3960+
<data name="HelpMessage_MaximumNumberOfBackups" xml:space="preserve">
3961+
<value>Number of backups that are retained before the oldest one is deleted.</value>
3962+
</data>
39543963
</root>

Source/NETworkManager.Models/Network/NetworkInterface.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -527,7 +527,6 @@ private static void RemoveIPAddressFromNetworkInterface(NetworkInterfaceConfig c
527527

528528
#endregion
529529

530-
531530
#region Events
532531

533532
/// <summary>

Source/NETworkManager.Profiles/GroupInfo.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -311,4 +311,4 @@ public GroupInfo(GroupInfo group) : this(group.Name)
311311
[XmlIgnore] public SecureString SNMP_Auth { get; set; }
312312
public SNMPV3PrivacyProvider SNMP_PrivacyProvider { get; set; } = GlobalStaticConfiguration.SNMP_PrivacyProvider;
313313
[XmlIgnore] public SecureString SNMP_Priv { get; set; }
314-
}
314+
}

Source/NETworkManager.Profiles/GroupInfoSerializable.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ public GroupInfoSerializable()
1010

1111
public GroupInfoSerializable(GroupInfo profileGroup) : base(profileGroup)
1212
{
13+
1314
}
1415

1516
/// <summary>
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.ComponentModel;
4+
using System.Linq;
5+
using System.Runtime.CompilerServices;
6+
using System.Text.Json.Serialization;
7+
8+
namespace NETworkManager.Profiles;
9+
10+
/// <summary>
11+
/// Represents the data structure for a profile file, including versioning, backup information, and groups of profiles.
12+
/// </summary>
13+
/// <remarks>This class supports property change notification through the <see cref="INotifyPropertyChanged"/>
14+
/// interface, allowing consumers to track changes to its properties. The <see cref="ProfilesChanged"/> property
15+
/// indicates whether the data has been modified since it was last saved, but is not persisted when serializing the
16+
/// object. Use this class to manage and persist user profile data, including handling schema migrations via the <see
17+
/// cref="Version"/> property.</remarks>
18+
public class ProfileFileData : INotifyPropertyChanged
19+
{
20+
/// <summary>
21+
/// Occurs when a property value changes.
22+
/// </summary>
23+
public event PropertyChangedEventHandler PropertyChanged;
24+
25+
/// <summary>
26+
/// Helper method to raise the <see cref="PropertyChanged" /> event and mark data as changed.
27+
/// </summary>
28+
/// <param name="propertyName">Name of the property that changed.</param>
29+
private void OnPropertyChanged([CallerMemberName] string propertyName = null)
30+
{
31+
ProfilesChanged = true;
32+
33+
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
34+
}
35+
36+
/// <summary>
37+
/// Indicates if the profile file data has been modified and needs to be saved.
38+
/// This is not serialized to the file.
39+
/// </summary>
40+
[JsonIgnore]
41+
public bool ProfilesChanged { get; set; }
42+
43+
private int _version = 1;
44+
45+
/// <summary>
46+
/// Schema version for handling future migrations.
47+
/// </summary>
48+
public int Version
49+
{
50+
get => _version;
51+
set
52+
{
53+
if (value == _version)
54+
return;
55+
56+
_version = value;
57+
OnPropertyChanged();
58+
}
59+
}
60+
61+
private DateTime? _lastBackup;
62+
63+
/// <summary>
64+
/// Date of the last backup (used for daily backup tracking).
65+
/// </summary>
66+
public DateTime? LastBackup
67+
{
68+
get => _lastBackup;
69+
set
70+
{
71+
if (value == _lastBackup)
72+
return;
73+
74+
_lastBackup = value;
75+
OnPropertyChanged();
76+
}
77+
}
78+
79+
/// <summary>
80+
/// Working copy of groups containing profiles (runtime objects with SecureString passwords).
81+
/// This is the single source of truth for profile data in memory.
82+
/// Not serialized directly - use <see cref="GroupsSerializable"/> for JSON serialization.
83+
/// </summary>
84+
[JsonIgnore]
85+
public List<GroupInfo> Groups { get; set; } = [];
86+
87+
/// <summary>
88+
/// Serializable representation of groups for JSON persistence.
89+
/// Gets: Converts working Groups to serializable format.
90+
/// Sets: Converts deserialized data back to working Groups.
91+
/// </summary>
92+
[JsonPropertyName("Groups")]
93+
public List<GroupInfoSerializable> GroupsSerializable
94+
{
95+
get => [.. Groups.Select(g => new GroupInfoSerializable(g)
96+
{
97+
Profiles = [.. g.Profiles.Where(p => !p.IsDynamic).Select(p => new ProfileInfoSerializable(p)
98+
{
99+
RemoteDesktop_Password = p.RemoteDesktop_Password != null
100+
? Utilities.SecureStringHelper.ConvertToString(p.RemoteDesktop_Password)
101+
: string.Empty,
102+
RemoteDesktop_GatewayServerPassword = p.RemoteDesktop_GatewayServerPassword != null
103+
? Utilities.SecureStringHelper.ConvertToString(p.RemoteDesktop_GatewayServerPassword)
104+
: string.Empty,
105+
SNMP_Community = p.SNMP_Community != null
106+
? Utilities.SecureStringHelper.ConvertToString(p.SNMP_Community)
107+
: string.Empty,
108+
SNMP_Auth = p.SNMP_Auth != null
109+
? Utilities.SecureStringHelper.ConvertToString(p.SNMP_Auth)
110+
: string.Empty,
111+
SNMP_Priv = p.SNMP_Priv != null
112+
? Utilities.SecureStringHelper.ConvertToString(p.SNMP_Priv)
113+
: string.Empty
114+
})],
115+
RemoteDesktop_Password = g.RemoteDesktop_Password != null
116+
? Utilities.SecureStringHelper.ConvertToString(g.RemoteDesktop_Password)
117+
: string.Empty,
118+
RemoteDesktop_GatewayServerPassword = g.RemoteDesktop_GatewayServerPassword != null
119+
? Utilities.SecureStringHelper.ConvertToString(g.RemoteDesktop_GatewayServerPassword)
120+
: string.Empty,
121+
SNMP_Community = g.SNMP_Community != null
122+
? Utilities.SecureStringHelper.ConvertToString(g.SNMP_Community)
123+
: string.Empty,
124+
SNMP_Auth = g.SNMP_Auth != null
125+
? Utilities.SecureStringHelper.ConvertToString(g.SNMP_Auth)
126+
: string.Empty,
127+
SNMP_Priv = g.SNMP_Priv != null
128+
? Utilities.SecureStringHelper.ConvertToString(g.SNMP_Priv)
129+
: string.Empty
130+
}).Where(g => !g.IsDynamic)];
131+
132+
set
133+
{
134+
Groups = value?.Select(gs => new GroupInfo(gs)
135+
{
136+
Profiles = [.. (gs.Profiles ?? []).Select(ps => new ProfileInfo(ps)
137+
{
138+
TagsCollection = (ps.TagsCollection == null || ps.TagsCollection.Count == 0) && !string.IsNullOrEmpty(ps.Tags)
139+
? new Controls.ObservableSetCollection<string>(ps.Tags.Split([';'], StringSplitOptions.RemoveEmptyEntries))
140+
: ps.TagsCollection,
141+
RemoteDesktop_Password = !string.IsNullOrEmpty(ps.RemoteDesktop_Password)
142+
? Utilities.SecureStringHelper.ConvertToSecureString(ps.RemoteDesktop_Password)
143+
: null,
144+
RemoteDesktop_GatewayServerPassword = !string.IsNullOrEmpty(ps.RemoteDesktop_GatewayServerPassword)
145+
? Utilities.SecureStringHelper.ConvertToSecureString(ps.RemoteDesktop_GatewayServerPassword)
146+
: null,
147+
SNMP_Community = !string.IsNullOrEmpty(ps.SNMP_Community)
148+
? Utilities.SecureStringHelper.ConvertToSecureString(ps.SNMP_Community)
149+
: null,
150+
SNMP_Auth = !string.IsNullOrEmpty(ps.SNMP_Auth)
151+
? Utilities.SecureStringHelper.ConvertToSecureString(ps.SNMP_Auth)
152+
: null,
153+
SNMP_Priv = !string.IsNullOrEmpty(ps.SNMP_Priv)
154+
? Utilities.SecureStringHelper.ConvertToSecureString(ps.SNMP_Priv)
155+
: null
156+
})],
157+
RemoteDesktop_Password = !string.IsNullOrEmpty(gs.RemoteDesktop_Password)
158+
? Utilities.SecureStringHelper.ConvertToSecureString(gs.RemoteDesktop_Password)
159+
: null,
160+
RemoteDesktop_GatewayServerPassword = !string.IsNullOrEmpty(gs.RemoteDesktop_GatewayServerPassword)
161+
? Utilities.SecureStringHelper.ConvertToSecureString(gs.RemoteDesktop_GatewayServerPassword)
162+
: null,
163+
SNMP_Community = !string.IsNullOrEmpty(gs.SNMP_Community)
164+
? Utilities.SecureStringHelper.ConvertToSecureString(gs.SNMP_Community)
165+
: null,
166+
SNMP_Auth = !string.IsNullOrEmpty(gs.SNMP_Auth)
167+
? Utilities.SecureStringHelper.ConvertToSecureString(gs.SNMP_Auth)
168+
: null,
169+
SNMP_Priv = !string.IsNullOrEmpty(gs.SNMP_Priv)
170+
? Utilities.SecureStringHelper.ConvertToSecureString(gs.SNMP_Priv)
171+
: null
172+
}).ToList() ?? [];
173+
}
174+
}
175+
}

Source/NETworkManager.Profiles/ProfileInfoSerializable.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,21 @@
22

33
public class ProfileInfoSerializable : ProfileInfo
44
{
5+
/// <summary>
6+
/// Initializes a new instance of the <see cref="ProfileInfoSerializable"/> class.
7+
/// </summary>
58
public ProfileInfoSerializable()
69
{
10+
711
}
812

13+
/// <summary>
14+
/// Initializes a new instance of the <see cref="ProfileInfoSerializable"/> class using the specified profile information.
15+
/// </summary>
16+
/// <param name="profile">The <see cref="ProfileInfo" /> object that contains the profile data to be serialized. Cannot be null.</param>
917
public ProfileInfoSerializable(ProfileInfo profile) : base(profile)
1018
{
19+
1120
}
1221

1322
/// <summary>

0 commit comments

Comments
 (0)