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(); 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(); 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(SubdirsAspect.ARG_SUBDIRS_NAME, false); IsDryRun = GetArgumentValueOrDefault(DryAspect.ARG_DRY_NAME, false); OnlyEmpty = GetArgumentValueOrDefault(EmptyAspect.ARG_EMPTY_NAME, false); // Get search patterns SearchPatterns = FilterAspect.GetSearchPatterns(this); } protected override IEnumerable OnSetupArguments() { var list = new List(); // 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 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 errors) { try { var searchOption = IncludeSubdirectories ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; var allDirectories = new List(); 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 } }