Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions Source/NETworkManager.Localization/Resources/Strings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions Source/NETworkManager.Localization/Resources/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -3988,4 +3988,14 @@ You can copy the “settings.json” file from "{0}" to "{1}" to migrate your pr
<data name="EnterValidFolderPath" xml:space="preserve">
<value>Enter a valid folder path!</value>
</data>
<data name="ChangeLocationProfilesMessage" xml:space="preserve">
<value>The location is changed and the application is restarted afterwards.

You can copy your profile files from “{0}” to “{1}” to migrate your previous profiles, if necessary. The application must be closed for this to prevent the profiles from being overwritten.</value>
</data>
<data name="RestoreDefaultLocationProfilesMessage" xml:space="preserve">
<value>The default path is restored and the application is restarted afterwards.

You can copy your profile files from “{0}” to “{1}” to migrate your previous profiles, if necessary. The application must be closed for this to prevent the profiles from being overwritten.</value>
</data>
</root>
115 changes: 111 additions & 4 deletions Source/NETworkManager.Profiles/ProfileManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -197,14 +197,121 @@ private static void ProfilesUpdated(bool profilesChanged = true)

/// <summary>
/// Method to get the path of the profiles folder.
/// Priority: 1. Policy override, 2. Custom user path (from SettingsInfo), 3. Portable/default.
/// </summary>
/// <returns>Path to the profiles folder.</returns>
public static string GetProfilesFolderLocation()
{
return ConfigurationManager.Current.IsPortable
? Path.Combine(AssemblyManager.Current.Location, ProfilesFolderName)
: Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
AssemblyManager.Current.Name, ProfilesFolderName);
// 1. Policy override takes precedence (for IT administrators)
if (!string.IsNullOrWhiteSpace(PolicyManager.Current?.Profiles_FolderLocation))
{
var validatedPath = ValidateProfilesFolderPath(
PolicyManager.Current.Profiles_FolderLocation,
"Policy-provided",
"next priority");

if (validatedPath != null)
return validatedPath;
}

// 2. Custom user-configured path (not available in portable mode)
if (!ConfigurationManager.Current.IsPortable &&
!string.IsNullOrWhiteSpace(SettingsManager.Current?.Profiles_CustomProfilesFolderLocation))
{
var validatedPath = ValidateProfilesFolderPath(
SettingsManager.Current.Profiles_CustomProfilesFolderLocation,
"Custom",
"default location");

if (validatedPath != null)
return validatedPath;
}

// 3. Fall back to portable or default location
if (ConfigurationManager.Current.IsPortable)
return GetPortableProfilesFolderLocation();
else
return GetDefaultProfilesFolderLocation();
}

/// <summary>
/// Method to get the default profiles folder location in the user's Documents directory.
/// </summary>
/// <returns>Path to the default profiles folder location.</returns>
public static string GetDefaultProfilesFolderLocation()
{
return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
AssemblyManager.Current.Name, ProfilesFolderName);
}

/// <summary>
/// Method to get the portable profiles folder location (in the same directory as the application).
/// </summary>
/// <returns>Path to the portable profiles folder location.</returns>
public static string GetPortableProfilesFolderLocation()
{
return Path.Combine(AssemblyManager.Current.Location, ProfilesFolderName);
}

/// <summary>
/// Validates a profiles folder path for correctness and accessibility.
/// </summary>
/// <param name="path">The path to validate.</param>
/// <param name="pathSource">Description of the path source for logging (e.g., "Policy-provided", "Custom").</param>
/// <param name="fallbackMessage">Message describing what happens on validation failure (e.g., "next priority", "default location").</param>
/// <returns>The validated full path if valid; otherwise, null.</returns>
private static string ValidateProfilesFolderPath(string path, string pathSource, string fallbackMessage)
{
// Expand environment variables first (e.g. %userprofile%\profiles -> C:\Users\...\profiles)
path = Environment.ExpandEnvironmentVariables(path);

// Validate that the path is rooted (absolute)
if (!Path.IsPathRooted(path))
{
Log.Error($"{pathSource} Profiles_FolderLocation is not an absolute path: {path}. Falling back to {fallbackMessage}.");
return null;
}

// Validate that the path doesn't contain invalid characters
try
{
// This will throw ArgumentException, NotSupportedException, SecurityException, PathTooLongException, or IOException if the path is invalid
var fullPath = Path.GetFullPath(path);

// Check if the path is a directory (not a file)
if (File.Exists(fullPath))
{
Log.Error($"{pathSource} Profiles_FolderLocation is a file, not a directory: {path}. Falling back to {fallbackMessage}.");
return null;
}

return Path.TrimEndingDirectorySeparator(fullPath);
}
catch (ArgumentException ex)
{
Log.Error($"{pathSource} Profiles_FolderLocation contains invalid characters: {path}. Falling back to {fallbackMessage}.", ex);
return null;
}
catch (NotSupportedException ex)
{
Log.Error($"{pathSource} Profiles_FolderLocation format is not supported: {path}. Falling back to {fallbackMessage}.", ex);
return null;
}
catch (SecurityException ex)
{
Log.Error($"Insufficient permissions to access {pathSource} Profiles_FolderLocation: {path}. Falling back to {fallbackMessage}.", ex);
return null;
}
catch (PathTooLongException ex)
{
Log.Error($"{pathSource} Profiles_FolderLocation path is too long: {path}. Falling back to {fallbackMessage}.", ex);
return null;
}
catch (IOException ex)
{
Log.Error($"{pathSource} Profiles_FolderLocation caused an I/O error: {path}. Falling back to {fallbackMessage}.", ex);
return null;
}
}

/// <summary>
Expand Down
12 changes: 6 additions & 6 deletions Source/NETworkManager.Settings/LocalSettingsInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,23 +34,23 @@ private void OnPropertyChanged([CallerMemberName] string propertyName = null)
[JsonIgnore] public bool SettingsChanged { get; set; }

/// <summary>
/// Private field for the <see cref="SettingsFolderLocation" /> property."
/// Private field for the <see cref="Settings_FolderLocation" /> property."
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There’s an extra trailing quote in the XML doc comment (property."), which looks like a typo.

Suggested change
/// Private field for the <see cref="Settings_FolderLocation" /> property."
/// Private field for the <see cref="Settings_FolderLocation" /> property.

Copilot uses AI. Check for mistakes.
/// </summary>
private string _settingsFolderLocation;
private string _settings_FolderLocation;

/// <summary>
/// Location of the folder where the local settings file is stored.
/// This can be changed by the user to move the settings file to a different location.
/// </summary>
public string SettingsFolderLocation
public string Settings_FolderLocation
{
get => _settingsFolderLocation;
get => _settings_FolderLocation;
set
{
if (_settingsFolderLocation == value)
if (_settings_FolderLocation == value)
return;

_settingsFolderLocation = value;
_settings_FolderLocation = value;
OnPropertyChanged();
}
}
Comment on lines 41 to 56
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renaming this property to Settings_FolderLocation changes the serialized local settings JSON field name as well, so existing users’ local settings files containing SettingsFolderLocation won’t be picked up and their custom location will silently reset. Consider adding a backward-compatible deserialization path (e.g., an [JsonPropertyName("SettingsFolderLocation")] legacy property that populates Settings_FolderLocation, or a migration step in LocalSettingsManager.Load()).

Copilot uses AI. Check for mistakes.
Expand Down
7 changes: 5 additions & 2 deletions Source/NETworkManager.Settings/PolicyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ public class PolicyInfo
[JsonPropertyName("Update_CheckForUpdatesAtStartup")]
public bool? Update_CheckForUpdatesAtStartup { get; set; }

[JsonPropertyName("SettingsFolderLocation")]
public string? SettingsFolderLocation { get; set; }
[JsonPropertyName("Settings_FolderLocation")]
public string? Settings_FolderLocation { get; set; }

[JsonPropertyName("Profiles_FolderLocation")]
public string? Profiles_FolderLocation { get; set; }
Comment on lines +14 to +18
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renaming the JSON key to Settings_FolderLocation is a breaking change for existing config.json deployments that still use SettingsFolderLocation (it won’t deserialize into this property). If backward compatibility is desired, consider keeping an alias property for the old key (marked [Obsolete]) and mapping it to the new property during/after deserialization.

Copilot uses AI. Check for mistakes.
}
3 changes: 2 additions & 1 deletion Source/NETworkManager.Settings/PolicyManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ public static void Load()

// Log enabled settings
Log.Info($"System-wide policy - Update_CheckForUpdatesAtStartup: {Current.Update_CheckForUpdatesAtStartup?.ToString() ?? "Not set"}");
Log.Info($"System-wide policy - SettingsFolderLocation: {Current.SettingsFolderLocation ?? "Not set"}");
Log.Info($"System-wide policy - Settings_FolderLocation: {Current.Settings_FolderLocation ?? "Not set"}");
Log.Info($"System-wide policy - Profiles_FolderLocation: {Current.Profiles_FolderLocation ?? "Not set"}");
}
}
catch (Exception ex)
Expand Down
19 changes: 19 additions & 0 deletions Source/NETworkManager.Settings/SettingsInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,25 @@ public int Profiles_MaximumNumberOfBackups
}
}

private string _profiles_CustomProfilesFolderLocation;

/// <summary>
/// Custom profiles folder location set by the user.
/// When set, overrides the default profiles folder location.
/// </summary>
public string Profiles_CustomProfilesFolderLocation
{
get => _profiles_CustomProfilesFolderLocation;
set
{
if (value == _profiles_CustomProfilesFolderLocation)
return;

_profiles_CustomProfilesFolderLocation = value;
OnPropertyChanged();
}
}

// Settings
private bool _settings_IsDailyBackupEnabled = GlobalStaticConfiguration.Settings_IsDailyBackupEnabled;

Expand Down
22 changes: 11 additions & 11 deletions Source/NETworkManager.Settings/SettingsManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,10 @@ public static class SettingsManager
public static string GetSettingsFolderLocation()
{
// 1. Policy override takes precedence (for IT administrators)
if (!string.IsNullOrWhiteSpace(PolicyManager.Current?.SettingsFolderLocation))
if (!string.IsNullOrWhiteSpace(PolicyManager.Current?.Settings_FolderLocation))
{
var validatedPath = ValidateSettingsFolderPath(
PolicyManager.Current.SettingsFolderLocation,
PolicyManager.Current.Settings_FolderLocation,
"Policy-provided",
"next priority");

Expand All @@ -92,10 +92,10 @@ public static string GetSettingsFolderLocation()

// 2. Custom user-configured path (not available in portable mode)
if (!ConfigurationManager.Current.IsPortable &&
!string.IsNullOrWhiteSpace(LocalSettingsManager.Current?.SettingsFolderLocation))
!string.IsNullOrWhiteSpace(LocalSettingsManager.Current?.Settings_FolderLocation))
{
var validatedPath = ValidateSettingsFolderPath(
LocalSettingsManager.Current.SettingsFolderLocation,
LocalSettingsManager.Current.Settings_FolderLocation,
"Custom",
"default location");

Expand Down Expand Up @@ -144,7 +144,7 @@ private static string ValidateSettingsFolderPath(string path, string pathSource,
// Validate that the path is rooted (absolute)
if (!Path.IsPathRooted(path))
{
Log.Error($"{pathSource} SettingsFolderLocation is not an absolute path: {path}. Falling back to {fallbackMessage}.");
Log.Error($"{pathSource} Settings_FolderLocation is not an absolute path: {path}. Falling back to {fallbackMessage}.");
return null;
}

Expand All @@ -157,35 +157,35 @@ private static string ValidateSettingsFolderPath(string path, string pathSource,
// Check if the path is a directory (not a file)
if (File.Exists(fullPath))
{
Log.Error($"{pathSource} SettingsFolderLocation is a file, not a directory: {path}. Falling back to {fallbackMessage}.");
Log.Error($"{pathSource} Settings_FolderLocation is a file, not a directory: {path}. Falling back to {fallbackMessage}.");
return null;
}

return Path.TrimEndingDirectorySeparator(fullPath);
}
catch (ArgumentException ex)
{
Log.Error($"{pathSource} SettingsFolderLocation contains invalid characters: {path}. Falling back to {fallbackMessage}.", ex);
Log.Error($"{pathSource} Settings_FolderLocation contains invalid characters: {path}. Falling back to {fallbackMessage}.", ex);
return null;
}
catch (NotSupportedException ex)
{
Log.Error($"{pathSource} SettingsFolderLocation format is not supported: {path}. Falling back to {fallbackMessage}.", ex);
Log.Error($"{pathSource} Settings_FolderLocation format is not supported: {path}. Falling back to {fallbackMessage}.", ex);
return null;
}
catch (SecurityException ex)
{
Log.Error($"Insufficient permissions to access {pathSource} SettingsFolderLocation: {path}. Falling back to {fallbackMessage}.", ex);
Log.Error($"Insufficient permissions to access {pathSource} Settings_FolderLocation: {path}. Falling back to {fallbackMessage}.", ex);
return null;
}
catch (PathTooLongException ex)
{
Log.Error($"{pathSource} SettingsFolderLocation path is too long: {path}. Falling back to {fallbackMessage}.", ex);
Log.Error($"{pathSource} Settings_FolderLocation path is too long: {path}. Falling back to {fallbackMessage}.", ex);
return null;
}
catch (IOException ex)
{
Log.Error($"{pathSource} SettingsFolderLocation caused an I/O error: {path}. Falling back to {fallbackMessage}.", ex);
Log.Error($"{pathSource} Settings_FolderLocation caused an I/O error: {path}. Falling back to {fallbackMessage}.", ex);
return null;
}
}
Expand Down
3 changes: 2 additions & 1 deletion Source/NETworkManager.Settings/config.json.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"Update_CheckForUpdatesAtStartup": false,
"SettingsFolderLocation": "C:\\CustomPath\\NETworkManager\\Settings"
"Settings_FolderLocation": "C:\\CustomPath\\NETworkManager\\Settings",
"Profiles_FolderLocation": "C:\\CustomPath\\NETworkManager\\Profiles"
}
Loading