Explorar o código

qfu: add ClearDir, tested

Dalibor Votruba hai 1 ano
pai
achega
83b1638487

+ 23 - 0
qfu/Commands/Aspects/DryAspect.cs

@@ -0,0 +1,23 @@
+using Quadarax.Foundation.Core.QConsole.Argument;
+
+namespace qdr.app.tools.qfu.Commands.Aspects
+{
+    internal class DryAspect
+    {
+        #region *** Constants ***
+        public const string ARG_DRY_NAME = "dry";
+        private const string ARG_DRY_HINT = "dry";
+        private const string ARG_DRY_DESCR = "Flag if set then provide just clean simulation without removing.";
+        #endregion
+
+        #region *** Public Operations ***
+        public static IEnumerable<AbstractArgument> SetupArguments()
+        {
+            return
+            [
+                new FlagArgument(ARG_DRY_NAME, ARG_DRY_DESCR, ARG_DRY_HINT, false)
+            ];
+        }
+        #endregion
+    }
+}

+ 23 - 0
qfu/Commands/Aspects/EmptyAspect.cs

@@ -0,0 +1,23 @@
+using Quadarax.Foundation.Core.QConsole.Argument;
+
+namespace qdr.app.tools.qfu.Commands.Aspects
+{
+    internal class EmptyAspect
+    {
+        #region *** Constants ***
+        public const string ARG_EMPTY_NAME = "empty";
+        private const string ARG_EMPTY_HINT = "empty";
+        private const string ARG_EMPTY_DESCR = "Flag if set clean only empty files or directories depends on scope argument.";
+        #endregion
+
+        #region *** Public Operations ***
+        public static IEnumerable<AbstractArgument> SetupArguments()
+        {
+            return
+            [
+                new FlagArgument(ARG_EMPTY_NAME, ARG_EMPTY_DESCR, ARG_EMPTY_HINT, false)
+            ];
+        }
+        #endregion
+    }
+}

+ 52 - 0
qfu/Commands/Aspects/FilterAspect.cs

@@ -0,0 +1,52 @@
+using Quadarax.Foundation.Core.QConsole.Argument;
+using Quadarax.Foundation.Core.QConsole.Command.Base;
+
+namespace qdr.app.tools.qfu.Commands.Aspects
+{
+    internal class FilterAspect
+    {
+        #region *** Constants ***
+        private const string ARG_FILTER_SEP = "|";
+        public const string ARG_FILTER_NAME = "f";
+        private const string ARG_FILTER_HINT = "filter";
+        private const string ARG_FILTER_DESCR = "Search pattern for files or directories to delete (separated by pipe " + ARG_FILTER_SEP + "). Example: *.txt|*.log|temp*";
+        #endregion
+
+        #region *** Public Operations ***
+        public static IEnumerable<AbstractArgument> SetupArguments()
+        {
+            return
+            [
+                new NamedArgument(ARG_FILTER_NAME, ARG_FILTER_DESCR, ARG_FILTER_HINT, Quadarax.Foundation.Core.QConsole.Value.TypeValuesEnum.String, string.Empty, false)
+            ];
+        }
+
+        public static string[] GetSearchPatterns(AbstractCommand context)
+        {
+            if (context == null)
+                throw new ArgumentNullException(nameof(context), "Command context cannot be null.");
+
+            var filterArgument = context.Arguments.FirstOrDefault(x => string.Equals(x.Code, ARG_FILTER_NAME));
+            if (filterArgument?.Value.Get() == null)
+                return ["*"]; // Default pattern - match all
+
+            var filterValue = filterArgument.Value.Get()?.ToString();
+            if (string.IsNullOrWhiteSpace(filterValue))
+                return ["*"]; // Default pattern - match all
+
+            return ParseSearchPatterns(filterValue);
+        }
+
+        public static string[] ParseSearchPatterns(string filterArgument)
+        {
+            if (string.IsNullOrWhiteSpace(filterArgument))
+                return ["*"];
+
+            return filterArgument.Split(new[] { ARG_FILTER_SEP }, StringSplitOptions.RemoveEmptyEntries)
+                                 .Select(f => f.Trim())
+                                 .Where(f => !string.IsNullOrEmpty(f))
+                                 .ToArray();
+        }
+        #endregion
+    }
+}

+ 43 - 0
qfu/Commands/Aspects/ScopeAspect.cs

@@ -0,0 +1,43 @@
+using Quadarax.Foundation.Core.QConsole.Argument;
+
+namespace qdr.app.tools.qfu.Commands.Aspects
+{
+    internal class ScopeAspect
+    {
+        #region *** Constants ***
+        public const string ARG_SCOPE_NAME = "scope";
+        private const string ARG_SCOPE_HINT = "scope";
+        private const string ARG_SCOPE_DESCR = "Define scope to clean. Possible values '"+ SCOPE_FILES +"' or '"+ SCOPE_DIRECTORIES +"'.";
+        private const int ARG_SCOPE_ORD = 0;
+
+        public const string SCOPE_FILES = "files";
+        public const string SCOPE_DIRECTORIES = "directories";
+        #endregion
+
+        #region *** Public Operations ***
+        public static IEnumerable<AbstractArgument> SetupArguments()
+        {
+            return
+            [
+                new OrdinalArgument(ARG_SCOPE_ORD, false, ARG_SCOPE_NAME, ARG_SCOPE_DESCR, ARG_SCOPE_HINT, Quadarax.Foundation.Core.QConsole.Value.TypeValuesEnum.String, string.Empty, true)
+            ];
+        }
+
+        public static bool IsValidScope(string scope)
+        {
+            return string.Equals(scope, SCOPE_FILES, StringComparison.OrdinalIgnoreCase) ||
+                   string.Equals(scope, SCOPE_DIRECTORIES, StringComparison.OrdinalIgnoreCase);
+        }
+
+        public static bool IsScopeFiles(string scope)
+        {
+            return string.Equals(scope, SCOPE_FILES, StringComparison.OrdinalIgnoreCase);
+        }
+
+        public static bool IsScopeDirectories(string scope)
+        {
+            return string.Equals(scope, SCOPE_DIRECTORIES, StringComparison.OrdinalIgnoreCase);
+        }
+        #endregion
+    }
+}

+ 23 - 0
qfu/Commands/Aspects/SubdirsAspect.cs

@@ -0,0 +1,23 @@
+using Quadarax.Foundation.Core.QConsole.Argument;
+
+namespace qdr.app.tools.qfu.Commands.Aspects
+{
+    internal class SubdirsAspect
+    {
+        #region *** Constants ***
+        public const string ARG_SUBDIRS_NAME = "s";
+        private const string ARG_SUBDIRS_HINT = "subdirs";
+        private const string ARG_SUBDIRS_DESCR = "Flag if set, then provide operation in subdirectories, otherwise only in specified directory.";
+        #endregion
+
+        #region *** Public Operations ***
+        public static IEnumerable<AbstractArgument> SetupArguments()
+        {
+            return
+            [
+                new FlagArgument(ARG_SUBDIRS_NAME, ARG_SUBDIRS_DESCR, ARG_SUBDIRS_HINT, false)
+            ];
+        }
+        #endregion
+    }
+}

+ 274 - 0
qfu/Commands/ClearDirCmd.cs

@@ -0,0 +1,274 @@
+using qdr.app.tools.qfu.Commands.Aspects;
+using Quadarax.Foundation.Core.QConsole;
+using Quadarax.Foundation.Core.QConsole.Argument;
+using Quadarax.Foundation.Core.QConsole.Attributes;
+using Quadarax.Foundation.Core.Value;
+
+namespace qdr.app.tools.qfu.Commands
+{
+    [CommandDefinition]
+    internal class ClearDirCmd : BaseCmd
+    {
+        #region *** Constants ***
+        private const string CMD_NAME = "cleardir";
+        private const string CMD_DESCR = "Remove all files or directories (with optional search pattern) from directory specified and optionally its subdirectories.";
+        #endregion
+
+        #region *** Properties ***
+        public override string Name => CMD_NAME;
+        public override string Description => CMD_DESCR;
+
+        protected string Scope { get; private set; } = string.Empty;
+        protected string Directory { get; private set; } = string.Empty;
+        protected bool IncludeSubdirectories { get; private set; } = false;
+        protected string[] SearchPatterns { get; private set; } = Array.Empty<string>();
+        protected bool IsDryRun { get; private set; } = false;
+        protected bool OnlyEmpty { get; private set; } = false;
+        #endregion
+
+        #region *** Constructors ***
+        public ClearDirCmd(Engine engine) : base(engine)
+        {
+        }
+        #endregion
+
+        #region *** Overrides ***
+        #region **** EXECUTE ****
+        protected override Result OnExecute()
+        {
+            var scopeType = ScopeAspect.IsScopeFiles(Scope) ? "files" : "directories";
+            var subdirText = IncludeSubdirectories ? " and subdirectories" : "";
+            var dryText = IsDryRun ? " (DRY RUN)" : "";
+            var emptyText = OnlyEmpty ? " (empty only)" : "";
+            var patternsText = SearchPatterns.Length > 1 ? $" with patterns: {string.Join(", ", SearchPatterns)}" : 
+                              SearchPatterns.Length == 1 && SearchPatterns[0] != "*" ? $" with pattern: {SearchPatterns[0]}" : "";
+
+            WriteCaption($"Will be clearing {scopeType} from '{Directory}'{subdirText}{patternsText}{emptyText}{dryText}...");
+
+            var totalProcessed = 0;
+            var totalRemoved = 0;
+            var totalSkipped = 0;
+            var errors = new List<string>();
+
+            try
+            {
+                if (ScopeAspect.IsScopeFiles(Scope))
+                {
+                    ProcessFiles(Directory, ref totalProcessed, ref totalRemoved, ref totalSkipped, errors);
+                }
+                else
+                {
+                    ProcessDirectories(Directory, ref totalProcessed, ref totalRemoved, ref totalSkipped, errors);
+                }
+
+                WriteCaption($"Operation completed successfully.");
+                WriteCaption($"SUMMARY:");
+                WriteCaption($"  Total {scopeType}: {totalProcessed}");
+                WriteCaption($"  Deleted {scopeType}: {totalRemoved}");
+                WriteCaption($"  Skipped {scopeType}: {totalSkipped}");
+                if (errors.Count > 0)
+                    WriteCaption($"  Failed {scopeType}: {errors.Count}");
+                
+                if (errors.Count > 0)
+                {
+                    WriteWarning($"Encountered {errors.Count} errors during operation:");
+                    foreach (var error in errors.Take(10)) // Show first 10 errors
+                    {
+                        WriteError(error);
+                    }
+                    if (errors.Count > 10)
+                        WriteWarning($"... and {errors.Count - 10} more errors.");
+                }
+
+                return errors.Count == 0 ? new Result() : new Result(new Exception($"Failed to process {errors.Count} items."));
+            }
+            catch (Exception ex)
+            {
+                WriteError($"Critical error during operation: {ex.Message}");
+                return new Result(ex);
+            }
+        }
+        #endregion
+
+        #region **** Arguments ****
+        protected override void OnValidateArguments()
+        {
+            // Get and validate scope first
+            Scope = ArgumentAsFileName(ScopeAspect.ARG_SCOPE_NAME);
+            if (!ScopeAspect.IsValidScope(Scope))
+                throw new ArgumentException($"Invalid scope '{Scope}'. Valid values are 'files' or 'directories'.", ScopeAspect.ARG_SCOPE_NAME);
+
+            // Get and validate directory (from our custom directory argument)
+            Directory = ArgumentAsFileName("directory");
+            if (!FileSystem.Directory.Exists(Directory))
+                throw new ArgumentException($"Directory '{Directory}' does not exist.", "directory");
+
+            // Get flags
+            IncludeSubdirectories = GetArgumentValueOrDefault<bool>(SubdirsAspect.ARG_SUBDIRS_NAME, false);
+            IsDryRun = GetArgumentValueOrDefault<bool>(DryAspect.ARG_DRY_NAME, false);
+            OnlyEmpty = GetArgumentValueOrDefault<bool>(EmptyAspect.ARG_EMPTY_NAME, false);
+
+            // Get search patterns
+            SearchPatterns = FilterAspect.GetSearchPatterns(this);
+        }
+
+        protected override IEnumerable<AbstractArgument> OnSetupArguments()
+        {
+            var list = new List<AbstractArgument>();
+            
+            // Add arguments in the correct order: scope first, then directory
+            list.AddRange(ScopeAspect.SetupArguments());
+            
+            // Add directory argument (ordinal 1) - custom since we need different ordinal than SourceAspect
+            list.Add(new OrdinalArgument(1, false, "directory", "Directory to clean.", "directory", 
+                Quadarax.Foundation.Core.QConsole.Value.TypeValuesEnum.String, string.Empty, true));
+            
+            // Add optional arguments
+            list.AddRange(SubdirsAspect.SetupArguments());
+            list.AddRange(FilterAspect.SetupArguments());
+            list.AddRange(DryAspect.SetupArguments());
+            list.AddRange(EmptyAspect.SetupArguments());
+            
+            return list;
+        }
+        #endregion
+
+        #region **** Private Operations ****
+        private void ProcessFiles(string directory, ref int totalProcessed, ref int totalRemoved, ref int totalSkipped, List<string> errors)
+        {
+            try
+            {
+                var searchOption = IncludeSubdirectories ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
+                
+                foreach (var pattern in SearchPatterns)
+                {
+                    var files = FileSystem.Directory.GetFiles(directory, pattern, searchOption);
+                    
+                    foreach (var file in files)
+                    {
+                        totalProcessed++;
+                        
+                        try
+                        {
+                            if (OnlyEmpty)
+                            {
+                                var fileInfo = FileSystem.FileInfo.New(file);
+                                if (fileInfo.Length > 0)
+                                {
+                                    WriteInfo($"[{totalProcessed}] Skipping non-empty file '{file}' (size: {fileInfo.Length} bytes).");
+                                    totalSkipped++;
+                                    continue;
+                                }
+                            }
+
+                            if (IsDryRun)
+                            {
+                                WriteInfo($"[{totalProcessed}] Would delete file '{file}'.");
+                                totalRemoved++;
+                            }
+                            else
+                            {
+                                FileSystem.File.Delete(file);
+                                WriteInfo($"[{totalProcessed}] Deleted file '{file}'.");
+                                totalRemoved++;
+                            }
+                        }
+                        catch (Exception ex)
+                        {
+                            var errorMsg = $"[{totalProcessed}] Failed to delete file '{file}': {ex.Message}";
+                            errors.Add(errorMsg);
+                            WriteError(errorMsg);
+                        }
+                    }
+                }
+            }
+            catch (Exception ex)
+            {
+                var errorMsg = $"Failed to enumerate files in directory '{directory}': {ex.Message}";
+                errors.Add(errorMsg);
+                WriteError(errorMsg);
+            }
+        }
+
+        private void ProcessDirectories(string directory, ref int totalProcessed, ref int totalRemoved, ref int totalSkipped, List<string> errors)
+        {
+            try
+            {
+                var searchOption = IncludeSubdirectories ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
+                var allDirectories = new List<string>();
+
+                foreach (var pattern in SearchPatterns)
+                {
+                    var directories = FileSystem.Directory.GetDirectories(directory, pattern, searchOption);
+                    allDirectories.AddRange(directories);
+                }
+
+                // Remove duplicates and sort by depth (deepest first) to ensure proper deletion order
+                var uniqueDirectories = allDirectories.Distinct()
+                    .OrderByDescending(dir => dir.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar).Length)
+                    .ToArray();
+
+                foreach (var dir in uniqueDirectories)
+                {
+                    // Skip the root directory itself
+                    if (string.Equals(dir, directory, StringComparison.OrdinalIgnoreCase))
+                        continue;
+
+                    totalProcessed++;
+
+                    try
+                    {
+                        if (OnlyEmpty)
+                        {
+                            var isEmpty = IsDirectoryEmpty(dir);
+                            if (!isEmpty)
+                            {
+                                WriteInfo($"[{totalProcessed}] Skipping non-empty directory '{dir}'.");
+                                totalSkipped++;
+                                continue;
+                            }
+                        }
+
+                        if (IsDryRun)
+                        {
+                            WriteInfo($"[{totalProcessed}] Would delete directory '{dir}'.");
+                            totalRemoved++;
+                        }
+                        else
+                        {
+                            FileSystem.Directory.Delete(dir, !OnlyEmpty); // recursive delete if not OnlyEmpty
+                            WriteInfo($"[{totalProcessed}] Deleted directory '{dir}'.");
+                            totalRemoved++;
+                        }
+                    }
+                    catch (Exception ex)
+                    {
+                        var errorMsg = $"[{totalProcessed}] Failed to delete directory '{dir}': {ex.Message}";
+                        errors.Add(errorMsg);
+                        WriteError(errorMsg);
+                    }
+                }
+            }
+            catch (Exception ex)
+            {
+                var errorMsg = $"Failed to enumerate directories in '{directory}': {ex.Message}";
+                errors.Add(errorMsg);
+                WriteError(errorMsg);
+            }
+        }
+
+        private bool IsDirectoryEmpty(string directory)
+        {
+            try
+            {
+                return !FileSystem.Directory.EnumerateFileSystemEntries(directory).Any();
+            }
+            catch
+            {
+                return false; // If we can't enumerate, assume it's not empty
+            }
+        }
+        #endregion
+        #endregion
+    }
+}

+ 4 - 0
qfu/Properties/launchSettings.json

@@ -14,6 +14,10 @@
     "qfu clon n": {
       "commandName": "Project",
       "commandLineArgs": "clone test.dat \"test01.dat\"|\"test02.dat\"|\"test03.dat\" -n:10 -force"
+    },
+    "qfu cleardir": {
+      "commandName": "Project",
+      "commandLineArgs": "cleardir direcories testdir -s -dry"
     }
   }
 }

+ 8 - 1
qfu/directory_structure.txt

@@ -1,5 +1,5 @@
 Project Directory Structure 
-Generated on: 12.06.2025 18:46:19,06 
+Generated on: 13.06.2025 11:58:46,19 
 Root: d:\Projects\quadarax\qdr.app\qdr.app.tools\qfu 
  
 directory_structure.txt 
@@ -13,14 +13,21 @@ QfuCommandContext.cs
 QfuEngineContext.cs 
 readme.md 
 releasenotes.md 
+test_genstruct.cmd 
 Commands/ 
   BaseCmd.cs 
+  ClearDirCmd.cs 
   CloneCmd.cs 
   Aspects/ 
+    DryAspect.cs 
+    EmptyAspect.cs 
     FilesAspect.cs 
+    FilterAspect.cs 
     ForceAspect.cs 
     ListAspect.cs 
+    ScopeAspect.cs 
     SourceAspect.cs 
+    SubdirsAspect.cs 
 Properties/ 
   launchSettings.json 
   PublishProfiles/ 

+ 9 - 4
qfu/releasenotes.md

@@ -1,4 +1,9 @@
-* 1.0.0.
-# 12.6.2025
-- initial version
-- command 'clone' add
+# 1.0.0.
+ 13.6.2025
+- Initial version
+- Added clone command with file cloning capabilities
+- Added cleardir command with advanced directory cleaning
+- Implemented modular aspect-based architecture
+- Added comprehensive error handling and progress reporting
+- Implemented dry run mode for safe testing
+- Added pattern matching and filtering capabilities

+ 38 - 0
qfu/test_genstruct.cmd

@@ -0,0 +1,38 @@
+@echo off
+echo Creating test directory structure...
+
+REM Create all directories first
+mkdir testdir 2>nul
+mkdir testdir\dir1 2>nul
+mkdir testdir\dir1\dir11 2>nul
+mkdir testdir\dir1\dir12 2>nul
+mkdir testdir\dir1\dir12\dir13 2>nul
+mkdir testdir\dir1\dir12\dir14 2>nul
+mkdir testdir\dir2 2>nul
+mkdir testdir\dir3 2>nul
+mkdir testdir\dir3\xxx1 2>nul
+mkdir testdir\dir3\xxx1\yyy1 2>nul
+mkdir testdir\dir3\xxx1\yyy2 2>nul
+mkdir testdir\dir3\xxx2 2>nul
+
+REM Create 0-byte files (only if they don't exist)
+if not exist testdir\file.dat echo. 2>testdir\file.dat
+if not exist testdir\dir1\dir12\file2.dat echo. 2>testdir\dir1\dir12\file2.dat
+if not exist testdir\dir1\dir12\dir14\ddd.dat echo. 2>testdir\dir1\dir12\dir14\ddd.dat
+if not exist testdir\dir3\xxx1\yyy2\file.dat echo. 2>testdir\dir3\xxx1\yyy2\file.dat
+
+REM Create 1024-byte files (only if they don't exist)
+if not exist testdir\dir1\aaa.dat fsutil file createnew testdir\dir1\aaa.dat 1024
+if not exist testdir\dir1\bbb.dat fsutil file createnew testdir\dir1\bbb.dat 1024
+if not exist testdir\dir1\dir11\ccc.dat fsutil file createnew testdir\dir1\dir11\ccc.dat 1024
+if not exist testdir\dir1\dir12\file.dat fsutil file createnew testdir\dir1\dir12\file.dat 1024
+if not exist testdir\dir3\file.dat fsutil file createnew testdir\dir3\file.dat 1024
+
+echo.
+echo Directory structure created successfully!
+echo.
+echo To verify the structure, run:
+echo tree testdir /f
+echo.
+echo To check file sizes, run:
+echo dir testdir /s