Skip to content

Commit 360ad30

Browse files
committed
Feature: Env var for folder
1 parent 7ed0538 commit 360ad30

File tree

9 files changed

+133
-43
lines changed

9 files changed

+133
-43
lines changed

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

Lines changed: 9 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: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3985,4 +3985,7 @@ You can copy the “settings.json” file from "{0}" to "{1}" to migrate your pr
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>
3988+
<data name="EnterValidFolderPath" xml:space="preserve">
3989+
<value>Enter a valid folder path!</value>
3990+
</data>
39883991
</root>

Source/NETworkManager.Settings/SettingsManager.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,9 @@ public static string GetPortableSettingsFolderLocation()
139139
/// <returns>The validated full path if valid; otherwise, null.</returns>
140140
private static string ValidateSettingsFolderPath(string path, string pathSource, string fallbackMessage)
141141
{
142+
// Expand environment variables first (e.g. %userprofile%\settings -> C:\Users\...\settings)
143+
path = Environment.ExpandEnvironmentVariables(path);
144+
142145
// Validate that the path is rooted (absolute)
143146
if (!Path.IsPathRooted(path))
144147
{
@@ -159,7 +162,7 @@ private static string ValidateSettingsFolderPath(string path, string pathSource,
159162
return null;
160163
}
161164

162-
return fullPath;
165+
return fullPath.TrimEnd('\\');
163166
}
164167
catch (ArgumentException ex)
165168
{

Source/NETworkManager.Utilities/RegexHelper.cs

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,30 @@ public static partial class RegexHelper
8585
[GeneratedRegex($@"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|{SpecialRangeRegex})\.){{3}}((?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|{SpecialRangeRegex})$")]
8686
public static partial Regex IPv4AddressSpecialRangeRegex();
8787

88+
/// <summary>
89+
/// Provides a compiled regular expression that matches valid hostnames or fully qualified domain names (FQDNs) like
90+
/// server-01 or server-01.example.com.
91+
/// </summary>
92+
/// <returns> A <see cref="Regex"/> instance that matches valid hostnames or FQDNs.</returns>
93+
[GeneratedRegex($@"^{HostnameOrDomainValues}$")]
94+
public static partial Regex HostnameOrDomainRegex();
95+
96+
/// <summary>
97+
/// Creates a regular expression that matches a local directory path or one using environment variables,
98+
/// like "C:\Temp", "C:\My Settings", "%AppData%\settings".
99+
/// </summary>
100+
/// <returns>A <see cref="Regex"/> instance that matches valid local directory paths.</returns>
101+
[GeneratedRegex($@"^(?:%[^%]+%|[a-zA-Z]\:)(\\[a-zA-Z0-9_\-\s\.]+)+\\?$")]
102+
public static partial Regex DirectoryPathWithEnvironmentVariablesRegex();
103+
104+
/// <summary>
105+
/// Creates a regular expression that matches a UNC path like "\\server\share", "\\server\c$\settings".
106+
/// The share name may end with $ for hidden shares.
107+
/// </summary>
108+
/// <returns>A <see cref="Regex"/> instance that matches valid UNC paths.</returns>
109+
[GeneratedRegex($@"^\\\\[a-zA-Z0-9_\-\.]+(\\[a-zA-Z0-9_\-\s\.]+\$?)(\\[a-zA-Z0-9_\-\s\.]+)*\\?$")]
110+
public static partial Regex UncPathRegex();
111+
88112
// Match a MAC-Address 000000000000 00:00:00:00:00:00, 00-00-00-00-00-00-00 or 0000.0000.0000
89113
public const string MACAddressRegex =
90114
@"^^[A-Fa-f0-9]{12}$|^[A-Fa-f0-9]{2}(:|-){1}[A-Fa-f0-9]{2}(:|-){1}[A-Fa-f0-9]{2}(:|-){1}[A-Fa-f0-9]{2}(:|-){1}[A-Fa-f0-9]{2}(:|-){1}[A-Fa-f0-9]{2}$|^[A-Fa-f0-9]{4}.[A-Fa-f0-9]{4}.[A-Fa-f0-9]{4}$$";
@@ -107,16 +131,6 @@ public static partial class RegexHelper
107131
public const string SpecialRangeRegex =
108132
@"\[((?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|((?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)-(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)))([,]((?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|((?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)-(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))))*\]";
109133

110-
111-
112-
/// <summary>
113-
/// Provides a compiled regular expression that matches valid hostnames or fully qualified domain names (FQDNs) like
114-
/// server-01 or server-01.example.com.
115-
/// </summary>
116-
/// <returns> A <see cref="Regex"/> instance that matches valid hostnames or FQDNs.</returns>
117-
[GeneratedRegex($@"^{HostnameOrDomainValues}$")]
118-
public static partial Regex HostnameOrDomainRegex();
119-
120134
// Match a hostname with cidr like server-01.example.com/24
121135
public const string HostnameOrDomainWithCidrRegex = $@"^{HostnameOrDomainValues}\/{CidrRegexValues}$";
122136

@@ -134,9 +148,6 @@ public static partial class RegexHelper
134148
// Match a port between 1-65535
135149
public const string PortRegex = $@"^{PortValues}$";
136150

137-
// Match any filepath (like "c:\temp\") --> https://www.codeproject.com/Tips/216238/Regular-Expression-to-Validate-File-Path-and-Exten
138-
public const string FilePathRegex = @"^(?:[\w]\:|\\)(\\[a-z_\-\s0-9\.]+)+$";
139-
140151
// Match any fullname (like "c:\temp\test.txt") --> https://www.codeproject.com/Tips/216238/Regular-Expression-to-Validate-File-Path-and-Exten
141152
public const string FullNameRegex =
142153
@"^(?:[\w]\:|\\)(\\[a-zA-Z0-9_\-\s\.()~!@#$%^&=+';,{}\[\]]+)+\.[a-zA-z0-9]{1,4}$";
Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,32 @@
1-
using System;
2-
using System.Globalization;
3-
using System.Text.RegularExpressions;
1+
using System.Globalization;
42
using System.Windows.Controls;
53
using NETworkManager.Localization.Resources;
64
using NETworkManager.Utilities;
75

86
namespace NETworkManager.Validators;
97

108
/// <summary>
11-
/// Check if the string is a valid directory path (like "C:\Temp\" or "%AppData%\Temp"). The directory path does not
12-
/// have to exist on the local system.
9+
/// Provides a validation rule that determines whether a value represents a syntactically valid directory path,
10+
/// supporting the inclusion of environment variable references like %UserProfile%.
1311
/// </summary>
1412
public class DirectoryPathWithEnvironmentVariablesValidator : ValidationRule
1513
{
1614
/// <summary>
17-
/// Check if the string is a valid directory path (like "C:\Temp\" or "%AppData%\Temp"). The directory path does not
18-
/// have to exist on the local system.
19-
/// </summary>
20-
/// <param name="value">Directory path like "C:\test" or "%AppData%\test".</param>
21-
/// <param name="cultureInfo">Culture to use for validation.</param>
22-
/// <returns>True if the directory path is valid.</returns>
15+
/// Validates whether the specified value represents a valid directory path, allowing for the inclusion of
16+
/// environment variables like %UserProfile%.
17+
/// </summary>
18+
/// <param name="value">The value to validate as a directory path. May include environment variable references. Can be a string or an
19+
/// object convertible to a string.</param>
20+
/// <param name="cultureInfo">The culture-specific information relevant to the validation process. This parameter is not used in this
21+
/// implementation.</param>
22+
/// <returns>A ValidationResult that indicates whether the value is a valid directory path. Returns
23+
/// ValidationResult.ValidResult if the value is valid; otherwise, returns a ValidationResult with an error message.</returns>
2324
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
2425
{
25-
var path = Environment.ExpandEnvironmentVariables((string)value);
26+
var path = $"{value}";
2627

27-
return new Regex(RegexHelper.FilePathRegex, RegexOptions.IgnoreCase).IsMatch(path)
28+
return RegexHelper.DirectoryPathWithEnvironmentVariablesRegex().IsMatch(path) || RegexHelper.UncPathRegex().IsMatch(path)
2829
? ValidationResult.ValidResult
29-
: new ValidationResult(false, Strings.EnterValidFilePath);
30+
: new ValidationResult(false, Strings.EnterValidFolderPath);
3031
}
31-
}
32+
}

Source/NETworkManager/NETworkManager.csproj

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,6 @@
88
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
99
<PlatformTarget>x64</PlatformTarget>
1010
<SelfContained>false</SelfContained>
11-
<!-- Publish a single file can't be used when accessing assembly file path
12-
<PublishSingleFile>true</PublishSingleFile>
13-
<PublishReadyToRun>true</PublishReadyToRun>
14-
-->
1511
<CsWinRTWindowsMetadata>sdk</CsWinRTWindowsMetadata>
1612
<UseWPF>true</UseWPF>
1713
<UseWindowsForms>true</UseWindowsForms>
@@ -74,8 +70,7 @@
7470
<PackageReference Include="MahApps.Metro.IconPacks.Octicons" Version="6.2.1" />
7571
<PackageReference Include="MahApps.Metro.SimpleChildWindow" Version="2.2.1" />
7672
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3650.58" />
77-
<PackageReference Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
78-
73+
<PackageReference Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
7974
<PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.135" />
8075
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
8176
<PackageReference Include="Microsoft.PowerShell.SDK" Version="7.5.4" />

Source/NETworkManager/ViewModels/SettingsSettingsViewModel.cs

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ namespace NETworkManager.ViewModels;
1313
public class SettingsSettingsViewModel : ViewModelBase
1414
{
1515
#region Variables
16+
/// <summary>
17+
/// Gets or sets the action to execute when the associated object is closed.
18+
/// </summary>
1619
public Action CloseAction { get; set; }
1720

1821
/// <summary>
@@ -143,6 +146,9 @@ public int MaximumNumberOfBackups
143146

144147
#region Constructor, LoadSettings
145148

149+
/// <summary>
150+
/// Initializes a new instance of the <see cref="SettingsSettingsViewModel" /> class and loads the current settings.
151+
/// </summary>
146152
public SettingsSettingsViewModel()
147153
{
148154
_isLoading = true;
@@ -152,14 +158,13 @@ public SettingsSettingsViewModel()
152158
_isLoading = false;
153159
}
154160

161+
/// <summary>
162+
/// Loads the application settings from the current settings folder location.
163+
/// </summary>
155164
private void LoadSettings()
156165
{
157166
Location = SettingsManager.GetSettingsFolderLocation();
158167
IsDefaultLocation = string.Equals(Location, SettingsManager.GetDefaultSettingsFolderLocation(), StringComparison.OrdinalIgnoreCase);
159-
160-
Debug.WriteLine(Location);
161-
Debug.WriteLine(SettingsManager.GetDefaultSettingsFolderLocation());
162-
163168
IsDailyBackupEnabled = SettingsManager.Current.Settings_IsDailyBackupEnabled;
164169
MaximumNumberOfBackups = SettingsManager.Current.Settings_MaximumNumberOfBackups;
165170
}
@@ -168,15 +173,27 @@ private void LoadSettings()
168173

169174
#region ICommands & Actions
170175

176+
/// <summary>
177+
/// Gets the command that opens a location when executed.
178+
/// </summary>
171179
public ICommand OpenLocationCommand => new RelayCommand(_ => OpenLocationAction());
172180

181+
/// <summary>
182+
/// Opens the settings folder location in Windows Explorer.
183+
/// </summary>
173184
private static void OpenLocationAction()
174185
{
175186
Process.Start("explorer.exe", SettingsManager.GetSettingsFolderLocation());
176187
}
177188

189+
/// <summary>
190+
/// Gets the command that resets the application settings to their default values.
191+
/// </summary>
178192
public ICommand ResetSettingsCommand => new RelayCommand(_ => ResetSettingsAction());
179193

194+
/// <summary>
195+
/// Resets the application settings to their default values.
196+
/// </summary>
180197
private void ResetSettingsAction()
181198
{
182199
ResetSettings().ConfigureAwait(false);
@@ -185,8 +202,18 @@ private void ResetSettingsAction()
185202
#endregion
186203

187204
#region Methods
205+
/// <summary>
206+
/// Gets the command that opens the location folder selection dialog.
207+
/// </summary>
188208
public ICommand BrowseLocationFolderCommand => new RelayCommand(p => BrowseLocationFolderAction());
189209

210+
/// <summary>
211+
/// Opens a dialog that allows the user to select a folder location and updates the Location property with the
212+
/// selected path if the user confirms the selection.
213+
/// </summary>
214+
/// <remarks>If the Location property is set to a valid directory path, it is pre-selected in the dialog.
215+
/// This method does not return a value and is intended for use in a user interface context where folder selection
216+
/// is required.</remarks>
190217
private void BrowseLocationFolderAction()
191218
{
192219
using var dialog = new System.Windows.Forms.FolderBrowserDialog();
@@ -200,13 +227,28 @@ private void BrowseLocationFolderAction()
200227
Location = dialog.SelectedPath;
201228
}
202229

230+
/// <summary>
231+
/// Sets the location path based on the provided drag-and-drop input.
232+
/// </summary>
233+
/// <param name="path">The path to set as the location. This value cannot be null or empty.</param>
203234
public void SetLocationPathFromDragDrop(string path)
204235
{
205236
Location = path;
206237
}
207238

239+
/// <summary>
240+
/// Gets the command that initiates the action to change the location.
241+
/// </summary>
208242
public ICommand ChangeLocationCommand => new RelayCommand(_ => ChangeLocationAction().ConfigureAwait(false));
209243

244+
/// <summary>
245+
/// Prompts the user to confirm and then changes the location of the application's settings folder.
246+
/// </summary>
247+
/// <remarks>This method displays a confirmation dialog to the user before changing the settings folder
248+
/// location. If the user confirms, it saves the current settings, updates the settings folder location, and
249+
/// restarts the application to apply the changes. No action is taken if the user cancels the confirmation
250+
/// dialog.</remarks>
251+
/// <returns>A task that represents the asynchronous operation.</returns>
210252
private async Task ChangeLocationAction()
211253
{
212254
var result = await DialogHelper.ShowConfirmationMessageAsync(Application.Current.MainWindow,
@@ -230,8 +272,19 @@ private async Task ChangeLocationAction()
230272
(Application.Current.MainWindow as MainWindow)?.RestartApplication();
231273
}
232274

275+
/// <summary>
276+
/// Gets the command that restores the default location settings asynchronously.
277+
/// </summary>
233278
public ICommand RestoreDefaultLocationCommand => new RelayCommand(_ => RestoreDefaultLocationActionAsync().ConfigureAwait(false));
234279

280+
/// <summary>
281+
/// Restores the application's settings folder location to the default path after obtaining user confirmation.
282+
/// </summary>
283+
/// <remarks>This method prompts the user to confirm the restoration of the default settings location. If
284+
/// the user confirms, it saves the current settings, clears any custom location, and restarts the application to
285+
/// apply the changes. Use this method when you want to revert to the default settings folder and ensure all changes
286+
/// are properly saved and applied.</remarks>
287+
/// <returns>A task that represents the asynchronous operation.</returns>
235288
private async Task RestoreDefaultLocationActionAsync()
236289
{
237290
var result = await DialogHelper.ShowConfirmationMessageAsync(Application.Current.MainWindow,
@@ -255,6 +308,13 @@ private async Task RestoreDefaultLocationActionAsync()
255308
(Application.Current.MainWindow as MainWindow)?.RestartApplication();
256309
}
257310

311+
/// <summary>
312+
/// Resets the application settings to their default values and restarts the application after user confirmation.
313+
/// </summary>
314+
/// <remarks>Displays a confirmation dialog to the user before proceeding. If the user confirms, the
315+
/// settings are reinitialized to their defaults and the application is restarted. No action is taken if the user
316+
/// cancels the confirmation dialog.</remarks>
317+
/// <returns>A task that represents the asynchronous operation.</returns>
258318
private async Task ResetSettings()
259319
{
260320
var result = await DialogHelper.ShowConfirmationMessageAsync(Application.Current.MainWindow,

Source/NETworkManager/Views/SettingsSettingsView.xaml

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,11 @@
3838
</Style.Triggers>
3939
</Style>
4040
</TextBox.Style>
41-
4241
<TextBox.Text>
4342
<Binding Path="Location" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged">
4443
<Binding.ValidationRules>
4544
<validators:EmptyValidator ValidatesOnTargetUpdated="True" />
45+
<validators:DirectoryPathWithEnvironmentVariablesValidator ValidatesOnTargetUpdated="True" />
4646
</Binding.ValidationRules>
4747
</Binding>
4848
</TextBox.Text>
@@ -67,11 +67,19 @@
6767
</Style.Triggers>
6868
</Style>
6969
</StackPanel.Style>
70-
<Button Style="{StaticResource ImageWithTextButton}"
71-
Command="{Binding ChangeLocationCommand}"
70+
<Button Command="{Binding ChangeLocationCommand}"
7271
Visibility="{Binding IsLocationChanged, Converter={StaticResource BooleanToVisibilityCollapsedConverter}}"
7372
HorizontalAlignment="Left"
7473
Margin="0,0,10,0" >
74+
<Button.Style>
75+
<Style TargetType="{x:Type Button}" BasedOn="{StaticResource ImageWithTextButton}">
76+
<Style.Triggers>
77+
<DataTrigger Binding="{Binding Path=(Validation.HasError), ElementName=TextBoxLocation}" Value="True">
78+
<Setter Property="IsEnabled" Value="False" />
79+
</DataTrigger>
80+
</Style.Triggers>
81+
</Style>
82+
</Button.Style>
7583
<Button.Content>
7684
<Grid>
7785
<Grid.ColumnDefinitions>

Website/docs/settings/settings.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ This setting can be controlled by administrators using a system-wide policy. See
2424
**Policy Property:** `SettingsFolderLocation`
2525

2626
**Values:**
27-
- Absolute path (e.g., `C:\\CustomPath\\NETworkManager\\Settings`) - Force a custom settings folder location for all users
27+
- Path like `C:\\Path\\To\\Settings` or `%UserProfile%\\NETworkManager\\Settings` - Force a custom settings folder location for all users
2828
- Omit the property - Allow the default location logic to apply (portable vs. non-portable)
2929

3030
**Example:**

0 commit comments

Comments
 (0)