diff --git a/src/PathLengthChecker.Tests/PathLengthChecker.Tests.csproj b/src/PathLengthChecker.Tests/PathLengthChecker.Tests.csproj index f77159d..e2c009d 100644 --- a/src/PathLengthChecker.Tests/PathLengthChecker.Tests.csproj +++ b/src/PathLengthChecker.Tests/PathLengthChecker.Tests.csproj @@ -1,121 +1,26 @@ - - - - + + - Debug - AnyCPU - - - 2.0 - {F2CE9DEF-4883-4C36-A37C-7B4840EBFEE0} - Library - Properties + net10.0 PathLengthChecker.Tests PathLengthChecker.Tests - v4.5.2 - 512 - {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - - - - - - - - - - - + enable + false + false - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - false - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - false - - - - ..\packages\FluentAssertions.5.10.3\lib\net45\FluentAssertions.dll - - - - - - - 3.5 - - - - - - ..\packages\System.ValueTuple.4.5.0\lib\netstandard1.0\System.ValueTuple.dll - - - - - ..\packages\xunit.abstractions.2.0.3\lib\net35\xunit.abstractions.dll - - - ..\packages\xunit.assert.2.4.1\lib\netstandard1.1\xunit.assert.dll - - - ..\packages\xunit.extensibility.core.2.4.1\lib\net452\xunit.core.dll - - - ..\packages\xunit.extensibility.execution.2.4.1\lib\net452\xunit.execution.desktop.dll - - - - - False - - - - - - - - - {4a51d771-dc33-4a66-82f9-c56bbd4e7004} - PathLengthChecker - - + - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + - + - - - - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - - - - - + \ No newline at end of file diff --git a/src/PathLengthChecker.Tests/PathLengthCheckerTests.cs b/src/PathLengthChecker.Tests/PathLengthCheckerTests.cs index 2b8bcd1..f0290ab 100644 --- a/src/PathLengthChecker.Tests/PathLengthCheckerTests.cs +++ b/src/PathLengthChecker.Tests/PathLengthCheckerTests.cs @@ -65,7 +65,7 @@ private void CreateDirectoriesAndFiles() { var filePath = Path.Combine(RootPath, file.Path); if (!File.Exists(filePath)) - File.Create(filePath); + File.Create(filePath).Close(); } } diff --git a/src/PathLengthChecker.Tests/Properties/AssemblyInfo.cs b/src/PathLengthChecker.Tests/Properties/AssemblyInfo.cs deleted file mode 100644 index f491fbc..0000000 --- a/src/PathLengthChecker.Tests/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("PathLengthChecker.Tests")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("Microsoft")] -[assembly: AssemblyProduct("PathLengthChecker.Tests")] -[assembly: AssemblyCopyright("Copyright © Microsoft 2012")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("d68636a5-15a4-4cd2-8a2b-f82d6e974934")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/PathLengthChecker.Tests/app.config b/src/PathLengthChecker.Tests/app.config deleted file mode 100644 index 557095a..0000000 --- a/src/PathLengthChecker.Tests/app.config +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/src/PathLengthChecker.Tests/packages.config b/src/PathLengthChecker.Tests/packages.config deleted file mode 100644 index c63a12c..0000000 --- a/src/PathLengthChecker.Tests/packages.config +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/PathLengthChecker/PathLengthChecker.csproj b/src/PathLengthChecker/PathLengthChecker.csproj index 0cdab61..ee02b12 100644 --- a/src/PathLengthChecker/PathLengthChecker.csproj +++ b/src/PathLengthChecker/PathLengthChecker.csproj @@ -1,67 +1,13 @@ - - - + + - Debug - AnyCPU - {4A51D771-DC33-4A66-82F9-C56BBD4E7004} Exe + net10.0 PathLengthChecker PathLengthChecker - v4.5.2 - 512 - true - true + enable + 1.4.0 + false - - AnyCPU - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - AnyCPU - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - ..\packages\AlphaFS.2.2.6\lib\net452\AlphaFS.dll - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/src/PathLengthChecker/PathRetriever.cs b/src/PathLengthChecker/PathRetriever.cs index caff385..d64971a 100644 --- a/src/PathLengthChecker/PathRetriever.cs +++ b/src/PathLengthChecker/PathRetriever.cs @@ -1,9 +1,7 @@ -using Alphaleonis.Win32.Filesystem; +using System; using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; +using System.IO; using System.Threading; -using SearchOption = System.IO.SearchOption; namespace PathLengthChecker { @@ -20,15 +18,34 @@ public static IEnumerable GetPaths(PathSearchOptions searchOptions, Canc { if (!Directory.Exists(searchOptions.RootDirectory)) { - throw new System.IO.DirectoryNotFoundException($"The specified root directory '{searchOptions.RootDirectory}' does not exist. Please provide a valid directory."); + throw new DirectoryNotFoundException($"The specified root directory '{searchOptions.RootDirectory}' does not exist. Please provide a valid directory."); } // If no Search Pattern was provided, then find everything. if (string.IsNullOrEmpty(searchOptions.SearchPattern)) searchOptions.SearchPattern = "*"; - // Get the paths according to the search parameters - var paths = GetPathsUsingAlphaFs(searchOptions); + var enumerationOptions = new EnumerationOptions + { + RecurseSubdirectories = searchOptions.SearchOption == SearchOption.AllDirectories, + IgnoreInaccessible = true, + AttributesToSkip = FileAttributes.ReparsePoint, + MatchCasing = MatchCasing.PlatformDefault, + }; + + IEnumerable paths; + switch (searchOptions.TypesToGet) + { + case FileSystemTypes.Files: + paths = Directory.EnumerateFiles(searchOptions.RootDirectory, searchOptions.SearchPattern, enumerationOptions); + break; + case FileSystemTypes.Directories: + paths = Directory.EnumerateDirectories(searchOptions.RootDirectory, searchOptions.SearchPattern, enumerationOptions); + break; + default: + paths = Directory.EnumerateFileSystemEntries(searchOptions.RootDirectory, searchOptions.SearchPattern, enumerationOptions); + break; + } // Return each of the paths, replacing the Root Directory if specified to do so. foreach (var path in paths) @@ -37,52 +54,16 @@ public static IEnumerable GetPaths(PathSearchOptions searchOptions, Canc if (cancellationToken.IsCancellationRequested) yield break; - var potentiallyTransformedPath = path; + var transformedPath = path; if (searchOptions.RootDirectoryReplacement != null) - potentiallyTransformedPath = potentiallyTransformedPath.Replace(searchOptions.RootDirectory, searchOptions.RootDirectoryReplacement); + transformedPath = transformedPath.Replace(searchOptions.RootDirectory, searchOptions.RootDirectoryReplacement); if (searchOptions.UrlEncodePaths) - potentiallyTransformedPath = System.Uri.EscapeDataString(potentiallyTransformedPath); - - yield return potentiallyTransformedPath; - } - } - - private static IEnumerable GetPathsUsingAlphaFs(PathSearchOptions searchOptions) - { - DirectoryEnumerationOptions options = (DirectoryEnumerationOptions)searchOptions.TypesToGet | - DirectoryEnumerationOptions.ContinueOnException | DirectoryEnumerationOptions.SkipReparsePoints; + transformedPath = Uri.EscapeDataString(transformedPath); - if (searchOptions.SearchOption == SearchOption.AllDirectories) - options |= DirectoryEnumerationOptions.Recursive; - - var paths = Directory.EnumerateFileSystemEntries(searchOptions.RootDirectory, searchOptions.SearchPattern, options); - return paths; - } - - private static IEnumerable GetPathsUsingSystemIo(PathSearchOptions searchOptions) - { - // Get the paths according to the search parameters - var paths = Enumerable.Empty(); - - switch (searchOptions.TypesToGet) - { - default: - case FileSystemTypes.All: - paths = Directory.GetFileSystemEntries(searchOptions.RootDirectory, searchOptions.SearchPattern, searchOptions.SearchOption); - break; - - case FileSystemTypes.Directories: - paths = Directory.GetDirectories(searchOptions.RootDirectory, searchOptions.SearchPattern, searchOptions.SearchOption); - break; - - case FileSystemTypes.Files: - paths = Directory.GetFiles(searchOptions.RootDirectory, searchOptions.SearchPattern, searchOptions.SearchOption); - break; + yield return transformedPath; } - - return paths; } } } diff --git a/src/PathLengthChecker/Properties/AssemblyInfo.cs b/src/PathLengthChecker/Properties/AssemblyInfo.cs deleted file mode 100644 index 80b8bbd..0000000 --- a/src/PathLengthChecker/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("PathLengthChecker")] -[assembly: AssemblyDescription("Path Length Checker is a stand-alone app that returns the paths and length of all files and directories in a given directory.")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("https://github.com/deadlydog/PathLengthChecker")] -[assembly: AssemblyProduct("PathLengthChecker")] -[assembly: AssemblyCopyright("MIT License")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("4a51d771-dc33-4a66-82f9-c56bbd4e7004")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/PathLengthChecker/app.config b/src/PathLengthChecker/app.config deleted file mode 100644 index d740e88..0000000 --- a/src/PathLengthChecker/app.config +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/src/PathLengthChecker/packages.config b/src/PathLengthChecker/packages.config deleted file mode 100644 index 168249c..0000000 --- a/src/PathLengthChecker/packages.config +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/src/PathLengthCheckerGUI/App.axaml b/src/PathLengthCheckerGUI/App.axaml new file mode 100644 index 0000000..82f4143 --- /dev/null +++ b/src/PathLengthCheckerGUI/App.axaml @@ -0,0 +1,9 @@ + + + + + + diff --git a/src/PathLengthCheckerGUI/App.axaml.cs b/src/PathLengthCheckerGUI/App.axaml.cs new file mode 100644 index 0000000..e54d1fe --- /dev/null +++ b/src/PathLengthCheckerGUI/App.axaml.cs @@ -0,0 +1,78 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; +using PathLengthChecker; +using System; +using System.IO; +using System.Threading.Tasks; + +namespace PathLengthCheckerGUI +{ + public partial class App : Application + { + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + SetupUnhandledExceptionHandling(); + + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + var mainWindow = new MainWindow(); + desktop.MainWindow = mainWindow; + + // Handle command-line arguments after the window opens so dialogs can be shown. + var args = desktop.Args ?? Array.Empty(); + mainWindow.Opened += (s, e) => HandleArgs(mainWindow, args); + } + + base.OnFrameworkInitializationCompleted(); + } + + private static async void HandleArgs(MainWindow mainWindow, string[] args) + { + if (args.Length == 1 && Directory.Exists(args[0])) + { + mainWindow.ViewModel.RootDirectory = args[0]; + await mainWindow.SearchForPaths(); + } + else if (args.Length >= 1) + { + try + { + var searchOptions = ArgumentParser.ParseArgs(args); + mainWindow.SetUIControlsFromSearchOptions(searchOptions); + + if (!string.IsNullOrEmpty(searchOptions?.RootDirectory)) + await mainWindow.SearchForPaths(); + } + catch (ArgumentException ex) + { + await MessageBoxHelper.ShowAsync(mainWindow, "Incorrect arguments", + $"Incorrectly-formatted arguments were passed to the program.\n\n{ex.Message}\n\n{ArgumentParser.ArgumentUsage}"); + } + } + } + + private void SetupUnhandledExceptionHandling() + { + AppDomain.CurrentDomain.UnhandledException += (sender, args) => + ShowUnhandledException(args.ExceptionObject as Exception, "AppDomain.CurrentDomain.UnhandledException"); + + TaskScheduler.UnobservedTaskException += (sender, args) => + ShowUnhandledException(args.Exception, "TaskScheduler.UnobservedTaskException"); + } + + private static async void ShowUnhandledException(Exception? e, string unhandledExceptionType) + { + var title = $"Unexpected Error: {unhandledExceptionType}"; + var message = $"The following exception occurred:\n\n{e}"; + + if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: Avalonia.Controls.Window mainWindow }) + await MessageBoxHelper.ShowAsync(mainWindow, title, message); + } + } +} diff --git a/src/PathLengthCheckerGUI/App.xaml b/src/PathLengthCheckerGUI/App.xaml deleted file mode 100644 index e07f522..0000000 --- a/src/PathLengthCheckerGUI/App.xaml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/src/PathLengthCheckerGUI/App.xaml.cs b/src/PathLengthCheckerGUI/App.xaml.cs deleted file mode 100644 index 49b1a1b..0000000 --- a/src/PathLengthCheckerGUI/App.xaml.cs +++ /dev/null @@ -1,118 +0,0 @@ -using System; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using System.Windows; -using System.Windows.Controls; -using PathLengthChecker; - -namespace PathLengthCheckerGUI -{ - /// - /// Interaction logic for App.xaml - /// - public partial class App : Application - { - public App() : base() - { - SetupUnhandledExceptionHandling(); - } - - private void SetupUnhandledExceptionHandling() - { - // Catch exceptions from all threads in the AppDomain. - AppDomain.CurrentDomain.UnhandledException += (sender, args) => - ShowUnhandledException(args.ExceptionObject as Exception, "AppDomain.CurrentDomain.UnhandledException", false); - - // Catch exceptions from each AppDomain that uses a task scheduler for async operations. - TaskScheduler.UnobservedTaskException += (sender, args) => - ShowUnhandledException(args.Exception, "TaskScheduler.UnobservedTaskException", false); - - // Catch exceptions from a single specific UI dispatcher thread. - Dispatcher.UnhandledException += (sender, args) => - { - // If we are debugging, let Visual Studio handle the exception and take us to the code that threw it. - if (!Debugger.IsAttached) - { - args.Handled = true; - ShowUnhandledException(args.Exception, "Dispatcher.UnhandledException", true); - } - }; - - // Catch exceptions from the main UI dispatcher thread. - // Typically we only need to catch this OR the Dispatcher.UnhandledException. - // Handling both can result in the exception getting handled twice. - //Application.Current.DispatcherUnhandledException += (sender, args) => - //{ - // // If we are debugging, let Visual Studio handle the exception and take us to the code that threw it. - // if (!Debugger.IsAttached) - // { - // args.Handled = true; - // ShowUnhandledException(args.Exception, "Application.Current.DispatcherUnhandledException", true); - // } - //}; - } - - void ShowUnhandledException(Exception e, string unhandledExceptionType, bool promptUserForShutdown) - { - var messageBoxTitle = $"Unexpected Error Occurred: {unhandledExceptionType}"; - var messageBoxMessage = $"The following exception occurred:\n\n{e}"; - var messageBoxButtons = MessageBoxButton.OK; - - if (promptUserForShutdown) - { - messageBoxMessage += "\n\nNormally the app would die now. Should we let it die?"; - messageBoxButtons = MessageBoxButton.YesNo; - } - - // Let the user decide if the app should die or not (if applicable). - if (MessageBox.Show(messageBoxMessage, messageBoxTitle, messageBoxButtons) == MessageBoxResult.Yes) - { - Application.Current.Shutdown(); - } - } - - private void Application_Startup(object sender, StartupEventArgs e) - { - InitializeComponent(); - var mainWindow = new MainWindow(); - - // If a directory was drag-and-dropped onto the GUI executable, launch with the app searching the given directory. - if (e.Args.Length == 1 && Directory.Exists(e.Args[0])) - { - mainWindow.txtRootDirectory.Text = e.Args[0]; - mainWindow.btnGetPathLengths.RaiseEvent(new RoutedEventArgs(Button.ClickEvent)); - } - else if (e.Args.Length >= 1) - { - try - { - var searchOptions = PathLengthChecker.ArgumentParser.ParseArgs(e.Args); - mainWindow.SetUIControlsFromSearchOptions(searchOptions); - - // Only start the search if a root dir was specified - if (!String.IsNullOrEmpty(searchOptions?.RootDirectory)) - { - mainWindow.btnGetPathLengths.RaiseEvent(new RoutedEventArgs(Button.ClickEvent)); - } - } - catch (ArgumentException ex) - { - string title = "Incorrect arguments"; - string message = "Incorrectly-formatted arguments were passed to the program.\n\n"; - message += ex.Message + "\n\n" + PathLengthChecker.ArgumentParser.ArgumentUsage; - MessageBox.Show(message, title); - } - } - - mainWindow.Show(); - } - - private void Application_Exit(object sender, ExitEventArgs e) - { - // Save any application settings that were changed when exiting (such as window size and position). - PathLengthCheckerGUI.Properties.Settings.Default.Save(); - } - } -} diff --git a/src/PathLengthCheckerGUI/AppSettings.cs b/src/PathLengthCheckerGUI/AppSettings.cs new file mode 100644 index 0000000..834d14e --- /dev/null +++ b/src/PathLengthCheckerGUI/AppSettings.cs @@ -0,0 +1,74 @@ +using Avalonia.Controls; +using PathLengthChecker; +using System; +using System.IO; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace PathLengthCheckerGUI +{ + /// + /// Persists user preferences to a JSON file in the application data folder. + /// + public class AppSettings + { + private static readonly string SettingsFilePath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "PathLengthChecker", + "settings.json"); + + // Window settings + public double WindowWidth { get; set; } = 900; + public double WindowHeight { get; set; } = 700; + public double WindowLeftPosition { get; set; } = 0; + public double WindowTopPosition { get; set; } = 0; + [JsonConverter(typeof(JsonStringEnumConverter))] + public WindowState WindowState { get; set; } = WindowState.Normal; + + // Search options + public string RootDirectory { get; set; } = string.Empty; + public bool ReplaceRootDirectory { get; set; } = false; + [JsonConverter(typeof(JsonStringEnumConverter))] + public FileSystemTypes FileSystemTypesToInclude { get; set; } = FileSystemTypes.All; + public bool IncludeSubdirectories { get; set; } = true; + public int MinPathLength { get; set; } = 0; + public int MaxPathLength { get; set; } = PathLengthSearchOptions.MaximumPathLengthMaxValue; + public string SearchPattern { get; set; } = string.Empty; + public string RootDirectoryReplacementText { get; set; } = string.Empty; + public bool UrlEncodePaths { get; set; } = false; + + public static AppSettings Load() + { + try + { + if (File.Exists(SettingsFilePath)) + { + var json = File.ReadAllText(SettingsFilePath); + return JsonSerializer.Deserialize(json) ?? new AppSettings(); + } + } + catch + { + // Return defaults if the settings file can't be read. + } + return new AppSettings(); + } + + public void Save() + { + try + { + var directory = Path.GetDirectoryName(SettingsFilePath)!; + if (!Directory.Exists(directory)) + Directory.CreateDirectory(directory); + + var json = JsonSerializer.Serialize(this, new JsonSerializerOptions { WriteIndented = true }); + File.WriteAllText(SettingsFilePath, json); + } + catch + { + // Silently ignore save failures. + } + } + } +} diff --git a/src/PathLengthCheckerGUI/ApplicationSettingsBindingExtension.cs b/src/PathLengthCheckerGUI/ApplicationSettingsBindingExtension.cs deleted file mode 100644 index f98260e..0000000 --- a/src/PathLengthCheckerGUI/ApplicationSettingsBindingExtension.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows.Data; - -namespace PathLengthCheckerGUI -{ - // Binding extension that allows XAML to easily access application settings stored in the Properties\Settings.settings file. - public class ApplicationSettingsBindingExtension : Binding - { - public ApplicationSettingsBindingExtension() - { - Initialize(); - } - - public ApplicationSettingsBindingExtension(string path) : base(path) - { - Initialize(); - } - - private void Initialize() - { - this.Source = PathLengthCheckerGUI.Properties.Settings.Default; - this.Mode = BindingMode.TwoWay; - } - } -} diff --git a/src/PathLengthCheckerGUI/MainWindow.axaml b/src/PathLengthCheckerGUI/MainWindow.axaml new file mode 100644 index 0000000..86dfa25 --- /dev/null +++ b/src/PathLengthCheckerGUI/MainWindow.axaml @@ -0,0 +1,215 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - -