ClearDirCmd.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. using qdr.app.tools.qfu.Commands.Aspects;
  2. using Quadarax.Foundation.Core.QConsole;
  3. using Quadarax.Foundation.Core.QConsole.Argument;
  4. using Quadarax.Foundation.Core.QConsole.Attributes;
  5. using Quadarax.Foundation.Core.Value;
  6. namespace qdr.app.tools.qfu.Commands
  7. {
  8. [CommandDefinition]
  9. internal class ClearDirCmd : BaseCmd
  10. {
  11. #region *** Constants ***
  12. private const string CMD_NAME = "cleardir";
  13. private const string CMD_DESCR = "Remove all files or directories (with optional search pattern) from directory specified and optionally its subdirectories.";
  14. #endregion
  15. #region *** Properties ***
  16. public override string Name => CMD_NAME;
  17. public override string Description => CMD_DESCR;
  18. protected string Scope { get; private set; } = string.Empty;
  19. protected string Directory { get; private set; } = string.Empty;
  20. protected bool IncludeSubdirectories { get; private set; } = false;
  21. protected string[] SearchPatterns { get; private set; } = Array.Empty<string>();
  22. protected bool IsDryRun { get; private set; } = false;
  23. protected bool OnlyEmpty { get; private set; } = false;
  24. #endregion
  25. #region *** Constructors ***
  26. public ClearDirCmd(Engine engine) : base(engine)
  27. {
  28. }
  29. #endregion
  30. #region *** Overrides ***
  31. #region **** EXECUTE ****
  32. protected override Result OnExecute()
  33. {
  34. var scopeType = ScopeAspect.IsScopeFiles(Scope) ? "files" : "directories";
  35. var subdirText = IncludeSubdirectories ? " and subdirectories" : "";
  36. var dryText = IsDryRun ? " (DRY RUN)" : "";
  37. var emptyText = OnlyEmpty ? " (empty only)" : "";
  38. var patternsText = SearchPatterns.Length > 1 ? $" with patterns: {string.Join(", ", SearchPatterns)}" :
  39. SearchPatterns.Length == 1 && SearchPatterns[0] != "*" ? $" with pattern: {SearchPatterns[0]}" : "";
  40. WriteCaption($"Will be clearing {scopeType} from '{Directory}'{subdirText}{patternsText}{emptyText}{dryText}...");
  41. var totalProcessed = 0;
  42. var totalRemoved = 0;
  43. var totalSkipped = 0;
  44. var errors = new List<string>();
  45. try
  46. {
  47. if (ScopeAspect.IsScopeFiles(Scope))
  48. {
  49. ProcessFiles(Directory, ref totalProcessed, ref totalRemoved, ref totalSkipped, errors);
  50. }
  51. else
  52. {
  53. ProcessDirectories(Directory, ref totalProcessed, ref totalRemoved, ref totalSkipped, errors);
  54. }
  55. WriteCaption($"Operation completed successfully.");
  56. WriteCaption($"SUMMARY:");
  57. WriteCaption($" Total {scopeType}: {totalProcessed}");
  58. WriteCaption($" Deleted {scopeType}: {totalRemoved}");
  59. WriteCaption($" Skipped {scopeType}: {totalSkipped}");
  60. if (errors.Count > 0)
  61. WriteCaption($" Failed {scopeType}: {errors.Count}");
  62. if (errors.Count > 0)
  63. {
  64. WriteWarning($"Encountered {errors.Count} errors during operation:");
  65. foreach (var error in errors.Take(10)) // Show first 10 errors
  66. {
  67. WriteError(error);
  68. }
  69. if (errors.Count > 10)
  70. WriteWarning($"... and {errors.Count - 10} more errors.");
  71. }
  72. return errors.Count == 0 ? new Result() : new Result(new Exception($"Failed to process {errors.Count} items."));
  73. }
  74. catch (Exception ex)
  75. {
  76. WriteError($"Critical error during operation: {ex.Message}");
  77. return new Result(ex);
  78. }
  79. }
  80. #endregion
  81. #region **** Arguments ****
  82. protected override void OnValidateArguments()
  83. {
  84. // Get and validate scope first
  85. Scope = ArgumentAsFileName(ScopeAspect.ARG_SCOPE_NAME);
  86. if (!ScopeAspect.IsValidScope(Scope))
  87. throw new ArgumentException($"Invalid scope '{Scope}'. Valid values are 'files' or 'directories'.", ScopeAspect.ARG_SCOPE_NAME);
  88. // Get and validate directory (from our custom directory argument)
  89. Directory = ArgumentAsFileName("directory");
  90. if (!FileSystem.Directory.Exists(Directory))
  91. throw new ArgumentException($"Directory '{Directory}' does not exist.", "directory");
  92. // Get flags
  93. IncludeSubdirectories = GetArgumentValueOrDefault<bool>(SubdirsAspect.ARG_SUBDIRS_NAME, false);
  94. IsDryRun = GetArgumentValueOrDefault<bool>(DryAspect.ARG_DRY_NAME, false);
  95. OnlyEmpty = GetArgumentValueOrDefault<bool>(EmptyAspect.ARG_EMPTY_NAME, false);
  96. // Get search patterns
  97. SearchPatterns = FilterAspect.GetSearchPatterns(this);
  98. }
  99. protected override IEnumerable<AbstractArgument> OnSetupArguments()
  100. {
  101. var list = new List<AbstractArgument>();
  102. // Add arguments in the correct order: scope first, then directory
  103. list.AddRange(ScopeAspect.SetupArguments());
  104. // Add directory argument (ordinal 1) - custom since we need different ordinal than SourceAspect
  105. list.Add(new OrdinalArgument(1, false, "directory", "Directory to clean.", "directory",
  106. Quadarax.Foundation.Core.QConsole.Value.TypeValuesEnum.String, string.Empty, true));
  107. // Add optional arguments
  108. list.AddRange(SubdirsAspect.SetupArguments());
  109. list.AddRange(FilterAspect.SetupArguments());
  110. list.AddRange(DryAspect.SetupArguments());
  111. list.AddRange(EmptyAspect.SetupArguments());
  112. return list;
  113. }
  114. #endregion
  115. #region **** Private Operations ****
  116. private void ProcessFiles(string directory, ref int totalProcessed, ref int totalRemoved, ref int totalSkipped, List<string> errors)
  117. {
  118. try
  119. {
  120. var searchOption = IncludeSubdirectories ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
  121. foreach (var pattern in SearchPatterns)
  122. {
  123. var files = FileSystem.Directory.GetFiles(directory, pattern, searchOption);
  124. foreach (var file in files)
  125. {
  126. totalProcessed++;
  127. try
  128. {
  129. if (OnlyEmpty)
  130. {
  131. var fileInfo = FileSystem.FileInfo.New(file);
  132. if (fileInfo.Length > 0)
  133. {
  134. WriteInfo($"[{totalProcessed}] Skipping non-empty file '{file}' (size: {fileInfo.Length} bytes).");
  135. totalSkipped++;
  136. continue;
  137. }
  138. }
  139. if (IsDryRun)
  140. {
  141. WriteInfo($"[{totalProcessed}] Would delete file '{file}'.");
  142. totalRemoved++;
  143. }
  144. else
  145. {
  146. FileSystem.File.Delete(file);
  147. WriteInfo($"[{totalProcessed}] Deleted file '{file}'.");
  148. totalRemoved++;
  149. }
  150. }
  151. catch (Exception ex)
  152. {
  153. var errorMsg = $"[{totalProcessed}] Failed to delete file '{file}': {ex.Message}";
  154. errors.Add(errorMsg);
  155. WriteError(errorMsg);
  156. }
  157. }
  158. }
  159. }
  160. catch (Exception ex)
  161. {
  162. var errorMsg = $"Failed to enumerate files in directory '{directory}': {ex.Message}";
  163. errors.Add(errorMsg);
  164. WriteError(errorMsg);
  165. }
  166. }
  167. private void ProcessDirectories(string directory, ref int totalProcessed, ref int totalRemoved, ref int totalSkipped, List<string> errors)
  168. {
  169. try
  170. {
  171. var searchOption = IncludeSubdirectories ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
  172. var allDirectories = new List<string>();
  173. foreach (var pattern in SearchPatterns)
  174. {
  175. var directories = FileSystem.Directory.GetDirectories(directory, pattern, searchOption);
  176. allDirectories.AddRange(directories);
  177. }
  178. // Remove duplicates and sort by depth (deepest first) to ensure proper deletion order
  179. var uniqueDirectories = allDirectories.Distinct()
  180. .OrderByDescending(dir => dir.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar).Length)
  181. .ToArray();
  182. foreach (var dir in uniqueDirectories)
  183. {
  184. // Skip the root directory itself
  185. if (string.Equals(dir, directory, StringComparison.OrdinalIgnoreCase))
  186. continue;
  187. totalProcessed++;
  188. try
  189. {
  190. if (OnlyEmpty)
  191. {
  192. var isEmpty = IsDirectoryEmpty(dir);
  193. if (!isEmpty)
  194. {
  195. WriteInfo($"[{totalProcessed}] Skipping non-empty directory '{dir}'.");
  196. totalSkipped++;
  197. continue;
  198. }
  199. }
  200. if (IsDryRun)
  201. {
  202. WriteInfo($"[{totalProcessed}] Would delete directory '{dir}'.");
  203. totalRemoved++;
  204. }
  205. else
  206. {
  207. FileSystem.Directory.Delete(dir, !OnlyEmpty); // recursive delete if not OnlyEmpty
  208. WriteInfo($"[{totalProcessed}] Deleted directory '{dir}'.");
  209. totalRemoved++;
  210. }
  211. }
  212. catch (Exception ex)
  213. {
  214. var errorMsg = $"[{totalProcessed}] Failed to delete directory '{dir}': {ex.Message}";
  215. errors.Add(errorMsg);
  216. WriteError(errorMsg);
  217. }
  218. }
  219. }
  220. catch (Exception ex)
  221. {
  222. var errorMsg = $"Failed to enumerate directories in '{directory}': {ex.Message}";
  223. errors.Add(errorMsg);
  224. WriteError(errorMsg);
  225. }
  226. }
  227. private bool IsDirectoryEmpty(string directory)
  228. {
  229. try
  230. {
  231. return !FileSystem.Directory.EnumerateFileSystemEntries(directory).Any();
  232. }
  233. catch
  234. {
  235. return false; // If we can't enumerate, assume it's not empty
  236. }
  237. }
  238. #endregion
  239. #endregion
  240. }
  241. }