Skip to content

Commit 6791337

Browse files
CopilotBornToBeRootCopilot
authored
Add profile file location management with policy support and rename folder location properties (#3340)
* Initial plan * Add profile file location management with policy support and rename folder location properties Co-authored-by: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> * Chore: Update strings * Docs: ... * Docs: #3340 * Update config.json.example * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Chore: Minor improvements * Update SettingsSettingsView.xaml.cs * Chore: Minor refactoring * Chore: Updates * Feature: Improve view * Docs: Changelog / Update blog post * Save profiles before changing location to prevent writes to wrong path, fix doc comment Co-authored-by: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> * Update SettingsProfilesViewModel.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>
1 parent 74e5966 commit 6791337

File tree

22 files changed

+715
-223
lines changed

22 files changed

+715
-223
lines changed

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

Lines changed: 37 additions & 15 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: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3972,20 +3972,30 @@ If you click Cancel, the profile file will remain unencrypted.</value>
39723972
<data name="RestoreDefaultLocationQuestion" xml:space="preserve">
39733973
<value>Restore default location?</value>
39743974
</data>
3975-
<data name="RestoreDefaultLocationSettingsMessage" xml:space="preserve">
3976-
<value>The default path is restored and the application is restarted afterwards.
3975+
<data name="RestoreDefaultSettingsLocationMessage" xml:space="preserve">
3976+
<value>The default settings location is restored and the application is restarted afterwards.
39773977

39783978
You can copy the “settings.json” file from "{0}" to "{1}" to migrate your previous settings, if necessary. The application must be closed for this to prevent the settings from being overwritten.</value>
39793979
</data>
39803980
<data name="ChangeLocationQuestion" xml:space="preserve">
39813981
<value>Change location?</value>
39823982
</data>
3983-
<data name="ChangeLocationSettingsMessage" xml:space="preserve">
3984-
<value>The location is changed and the application is restarted afterwards.
3983+
<data name="ChangeSettingsLocationMessage" xml:space="preserve">
3984+
<value>The settings location is changed and the application is restarted afterwards.
39853985

39863986
You can copy the “settings.json” file from "{0}" to "{1}" to migrate your previous settings, if necessary. The application must be closed for this to prevent the settings from being overwritten.</value>
39873987
</data>
39883988
<data name="EnterValidFolderPath" xml:space="preserve">
39893989
<value>Enter a valid folder path!</value>
39903990
</data>
3991+
<data name="ChangeProfilesLocationMessage" xml:space="preserve">
3992+
<value>The profiles location is changed and the application is restarted afterwards.
3993+
3994+
You can copy your profile files from “{0}” to “{1}” to migrate your existing profiles, if necessary. The application must be closed for this to prevent the profiles from being overwritten.</value>
3995+
</data>
3996+
<data name="RestoreDefaultProfilesLocationMessage" xml:space="preserve">
3997+
<value>The default profiles location is restored and the application is restarted afterwards.
3998+
3999+
You can copy your profile files from “{0}” to “{1}” to migrate your existing profiles, if necessary. The application must be closed for this to prevent the profiles from being overwritten.</value>
4000+
</data>
39914001
</root>

Source/NETworkManager.Profiles/ProfileManager.cs

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -197,14 +197,65 @@ private static void ProfilesUpdated(bool profilesChanged = true)
197197

198198
/// <summary>
199199
/// Method to get the path of the profiles folder.
200+
/// Priority:
201+
/// 1. Policy override (for IT administrators)
202+
/// 2. Custom user-configured path (not available in portable mode)
203+
/// 3. Portable (same directory as the application) or default location (Documents folder)
200204
/// </summary>
201205
/// <returns>Path to the profiles folder.</returns>
202206
public static string GetProfilesFolderLocation()
203207
{
204-
return ConfigurationManager.Current.IsPortable
205-
? Path.Combine(AssemblyManager.Current.Location, ProfilesFolderName)
206-
: Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
207-
AssemblyManager.Current.Name, ProfilesFolderName);
208+
// 1. Policy override takes precedence (for IT administrators)
209+
if (!string.IsNullOrWhiteSpace(PolicyManager.Current?.Profiles_FolderLocation))
210+
{
211+
var validatedPath = DirectoryHelper.ValidateFolderPath(
212+
PolicyManager.Current.Profiles_FolderLocation,
213+
"Policy-provided",
214+
nameof(PolicyInfo.Profiles_FolderLocation),
215+
"next priority");
216+
217+
if (validatedPath != null)
218+
return validatedPath;
219+
}
220+
221+
// 2. Custom user-configured path (not available in portable mode)
222+
if (!ConfigurationManager.Current.IsPortable &&
223+
!string.IsNullOrWhiteSpace(SettingsManager.Current?.Profiles_FolderLocation))
224+
{
225+
var validatedPath = DirectoryHelper.ValidateFolderPath(
226+
SettingsManager.Current.Profiles_FolderLocation,
227+
"Custom",
228+
nameof(SettingsInfo.Profiles_FolderLocation),
229+
"default location");
230+
231+
if (validatedPath != null)
232+
return validatedPath;
233+
}
234+
235+
// 3. Fall back to portable or default location
236+
if (ConfigurationManager.Current.IsPortable)
237+
return GetPortableProfilesFolderLocation();
238+
else
239+
return GetDefaultProfilesFolderLocation();
240+
}
241+
242+
/// <summary>
243+
/// Method to get the default profiles folder location in the user's Documents directory.
244+
/// </summary>
245+
/// <returns>Path to the default profiles folder location.</returns>
246+
public static string GetDefaultProfilesFolderLocation()
247+
{
248+
return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
249+
AssemblyManager.Current.Name, ProfilesFolderName);
250+
}
251+
252+
/// <summary>
253+
/// Method to get the portable profiles folder location (in the same directory as the application).
254+
/// </summary>
255+
/// <returns>Path to the portable profiles folder location.</returns>
256+
public static string GetPortableProfilesFolderLocation()
257+
{
258+
return Path.Combine(AssemblyManager.Current.Location, ProfilesFolderName);
208259
}
209260

210261
/// <summary>

Source/NETworkManager.Settings/LocalSettingsInfo.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,23 +34,23 @@ private void OnPropertyChanged([CallerMemberName] string propertyName = null)
3434
[JsonIgnore] public bool SettingsChanged { get; set; }
3535

3636
/// <summary>
37-
/// Private field for the <see cref="SettingsFolderLocation" /> property."
37+
/// Private field for the <see cref="Settings_FolderLocation" /> property.
3838
/// </summary>
39-
private string _settingsFolderLocation;
39+
private string _settings_FolderLocation;
4040

4141
/// <summary>
4242
/// Location of the folder where the local settings file is stored.
4343
/// This can be changed by the user to move the settings file to a different location.
4444
/// </summary>
45-
public string SettingsFolderLocation
45+
public string Settings_FolderLocation
4646
{
47-
get => _settingsFolderLocation;
47+
get => _settings_FolderLocation;
4848
set
4949
{
50-
if (_settingsFolderLocation == value)
50+
if (_settings_FolderLocation == value)
5151
return;
5252

53-
_settingsFolderLocation = value;
53+
_settings_FolderLocation = value;
5454
OnPropertyChanged();
5555
}
5656
}

Source/NETworkManager.Settings/PolicyInfo.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ public class PolicyInfo
1111
[JsonPropertyName("Update_CheckForUpdatesAtStartup")]
1212
public bool? Update_CheckForUpdatesAtStartup { get; set; }
1313

14-
[JsonPropertyName("SettingsFolderLocation")]
15-
public string? SettingsFolderLocation { get; set; }
14+
[JsonPropertyName("Profiles_FolderLocation")]
15+
public string? Profiles_FolderLocation { get; set; }
16+
17+
[JsonPropertyName("Settings_FolderLocation")]
18+
public string? Settings_FolderLocation { get; set; }
1619
}

Source/NETworkManager.Settings/PolicyManager.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@ public static void Load()
8484

8585
// Log enabled settings
8686
Log.Info($"System-wide policy - Update_CheckForUpdatesAtStartup: {Current.Update_CheckForUpdatesAtStartup?.ToString() ?? "Not set"}");
87-
Log.Info($"System-wide policy - SettingsFolderLocation: {Current.SettingsFolderLocation ?? "Not set"}");
87+
Log.Info($"System-wide policy - Profiles_FolderLocation: {Current.Profiles_FolderLocation ?? "Not set"}");
88+
Log.Info($"System-wide policy - Settings_FolderLocation: {Current.Settings_FolderLocation ?? "Not set"}");
8889
}
8990
}
9091
catch (Exception ex)

Source/NETworkManager.Settings/SettingsInfo.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -576,7 +576,22 @@ public bool Experimental_EnableExperimentalFeatures
576576
}
577577
}
578578

579-
// Profiles
579+
// Profiles
580+
private string _profiles_FolderLocation;
581+
582+
public string Profiles_FolderLocation
583+
{
584+
get => _profiles_FolderLocation;
585+
set
586+
{
587+
if (value == _profiles_FolderLocation)
588+
return;
589+
590+
_profiles_FolderLocation = value;
591+
OnPropertyChanged();
592+
}
593+
}
594+
580595
private string _profiles_LastSelected;
581596

582597
public string Profiles_LastSelected

Source/NETworkManager.Settings/SettingsManager.cs

Lines changed: 12 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
using System;
66
using System.IO;
77
using System.Linq;
8-
using System.Security;
98
using System.Text.Json;
109
using System.Text.Json.Serialization;
1110
using System.Xml.Serialization;
@@ -74,16 +73,21 @@ public static class SettingsManager
7473

7574
/// <summary>
7675
/// Method to get the path of the settings folder.
76+
/// Priority:
77+
/// 1. Policy override (for IT administrators)
78+
/// 2. Custom user-configured path (not available in portable mode)
79+
/// 3. Portable (same directory as the application) or default location (Documents folder)
7780
/// </summary>
7881
/// <returns>Path to the settings folder.</returns>
7982
public static string GetSettingsFolderLocation()
8083
{
8184
// 1. Policy override takes precedence (for IT administrators)
82-
if (!string.IsNullOrWhiteSpace(PolicyManager.Current?.SettingsFolderLocation))
85+
if (!string.IsNullOrWhiteSpace(PolicyManager.Current?.Settings_FolderLocation))
8386
{
84-
var validatedPath = ValidateSettingsFolderPath(
85-
PolicyManager.Current.SettingsFolderLocation,
87+
var validatedPath = DirectoryHelper.ValidateFolderPath(
88+
PolicyManager.Current.Settings_FolderLocation,
8689
"Policy-provided",
90+
nameof(PolicyInfo.Settings_FolderLocation),
8791
"next priority");
8892

8993
if (validatedPath != null)
@@ -92,11 +96,12 @@ public static string GetSettingsFolderLocation()
9296

9397
// 2. Custom user-configured path (not available in portable mode)
9498
if (!ConfigurationManager.Current.IsPortable &&
95-
!string.IsNullOrWhiteSpace(LocalSettingsManager.Current?.SettingsFolderLocation))
99+
!string.IsNullOrWhiteSpace(LocalSettingsManager.Current?.Settings_FolderLocation))
96100
{
97-
var validatedPath = ValidateSettingsFolderPath(
98-
LocalSettingsManager.Current.SettingsFolderLocation,
101+
var validatedPath = DirectoryHelper.ValidateFolderPath(
102+
LocalSettingsManager.Current.Settings_FolderLocation,
99103
"Custom",
104+
nameof(LocalSettingsInfo.Settings_FolderLocation),
100105
"default location");
101106

102107
if (validatedPath != null)
@@ -129,67 +134,6 @@ public static string GetPortableSettingsFolderLocation()
129134
return Path.Combine(AssemblyManager.Current.Location, SettingsFolderName);
130135
}
131136

132-
/// <summary>
133-
/// Validates a settings folder path for correctness and accessibility.
134-
/// </summary>
135-
/// <param name="path">The path to validate.</param>
136-
/// <param name="pathSource">Description of the path source for logging (e.g., "Policy-provided", "Custom").</param>
137-
/// <param name="fallbackMessage">Message describing what happens on validation failure (e.g., "next priority", "default location").</param>
138-
/// <returns>The validated full path if valid; otherwise, null.</returns>
139-
private static string ValidateSettingsFolderPath(string path, string pathSource, string fallbackMessage)
140-
{
141-
// Expand environment variables first (e.g. %userprofile%\settings -> C:\Users\...\settings)
142-
path = Environment.ExpandEnvironmentVariables(path);
143-
144-
// Validate that the path is rooted (absolute)
145-
if (!Path.IsPathRooted(path))
146-
{
147-
Log.Error($"{pathSource} SettingsFolderLocation is not an absolute path: {path}. Falling back to {fallbackMessage}.");
148-
return null;
149-
}
150-
151-
// Validate that the path doesn't contain invalid characters
152-
try
153-
{
154-
// This will throw ArgumentException, NotSupportedException, SecurityException, PathTooLongException, or IOException if the path is invalid
155-
var fullPath = Path.GetFullPath(path);
156-
157-
// Check if the path is a directory (not a file)
158-
if (File.Exists(fullPath))
159-
{
160-
Log.Error($"{pathSource} SettingsFolderLocation is a file, not a directory: {path}. Falling back to {fallbackMessage}.");
161-
return null;
162-
}
163-
164-
return Path.TrimEndingDirectorySeparator(fullPath);
165-
}
166-
catch (ArgumentException ex)
167-
{
168-
Log.Error($"{pathSource} SettingsFolderLocation contains invalid characters: {path}. Falling back to {fallbackMessage}.", ex);
169-
return null;
170-
}
171-
catch (NotSupportedException ex)
172-
{
173-
Log.Error($"{pathSource} SettingsFolderLocation format is not supported: {path}. Falling back to {fallbackMessage}.", ex);
174-
return null;
175-
}
176-
catch (SecurityException ex)
177-
{
178-
Log.Error($"Insufficient permissions to access {pathSource} SettingsFolderLocation: {path}. Falling back to {fallbackMessage}.", ex);
179-
return null;
180-
}
181-
catch (PathTooLongException ex)
182-
{
183-
Log.Error($"{pathSource} SettingsFolderLocation path is too long: {path}. Falling back to {fallbackMessage}.", ex);
184-
return null;
185-
}
186-
catch (IOException ex)
187-
{
188-
Log.Error($"{pathSource} SettingsFolderLocation caused an I/O error: {path}. Falling back to {fallbackMessage}.", ex);
189-
return null;
190-
}
191-
}
192-
193137
/// <summary>
194138
/// Method to get the path of the settings backup folder.
195139
/// </summary>
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{
22
"Update_CheckForUpdatesAtStartup": false,
3-
"SettingsFolderLocation": "C:\\CustomPath\\NETworkManager\\Settings"
3+
"Profiles_FolderLocation": "\\\\Server\\Shared\\NETworkManager\\Profiles",
4+
"Settings_FolderLocation": "%UserProfile%\\NETworkManager\\Settings"
45
}

0 commit comments

Comments
 (0)