|
@@ -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
|
|
|
|
|
+ }
|
|
|
|
|
+}
|