using System; using System.Diagnostics; using System.IO; using System.Linq; #pragma warning disable IDE0005 using System.Windows; #pragma warning restore IDE0005 namespace Flow.Launcher.Plugin.SharedCommands { /// /// Commands that are useful to run on files... and folders! /// public static class FilesFolders { private const string FileExplorerProgramName = "explorer"; /// /// Copies the folder and all of its files and folders /// including subfolders to the target location /// /// /// /// public static void CopyAll(this string sourcePath, string targetPath, Func messageBoxExShow = null) { // Get the subdirectories for the specified directory. DirectoryInfo dir = new DirectoryInfo(sourcePath); if (!dir.Exists) { throw new DirectoryNotFoundException( "Source directory does not exist or could not be found: " + sourcePath); } try { DirectoryInfo[] dirs = dir.GetDirectories(); // If the destination directory doesn't exist, create it. if (!Directory.Exists(targetPath)) { Directory.CreateDirectory(targetPath); } // Get the files in the directory and copy them to the new location. FileInfo[] files = dir.GetFiles(); foreach (FileInfo file in files) { string temppath = Path.Combine(targetPath, file.Name); file.CopyTo(temppath, false); } // Recursively copy subdirectories by calling itself on each subdirectory until there are no more to copy foreach (DirectoryInfo subdir in dirs) { string temppath = Path.Combine(targetPath, subdir.Name); CopyAll(subdir.FullName, temppath, messageBoxExShow); } } catch (Exception) { #if DEBUG throw; #else messageBoxExShow ??= MessageBox.Show; messageBoxExShow(string.Format("Copying path {0} has failed, it will now be deleted for consistency", targetPath)); RemoveFolderIfExists(targetPath, messageBoxExShow); #endif } } /// /// Check if the files and directories are identical between /// and /// /// /// /// /// public static bool VerifyBothFolderFilesEqual(this string fromPath, string toPath, Func messageBoxExShow = null) { try { var fromDir = new DirectoryInfo(fromPath); var toDir = new DirectoryInfo(toPath); if (fromDir.GetFiles("*", SearchOption.AllDirectories).Length != toDir.GetFiles("*", SearchOption.AllDirectories).Length) return false; if (fromDir.GetDirectories("*", SearchOption.AllDirectories).Length != toDir.GetDirectories("*", SearchOption.AllDirectories).Length) return false; return true; } catch (Exception) { #if DEBUG throw; #else messageBoxExShow ??= MessageBox.Show; messageBoxExShow(string.Format("Unable to verify folders and files between {0} and {1}", fromPath, toPath)); return false; #endif } } /// /// Deletes a folder if it exists /// /// /// public static void RemoveFolderIfExists(this string path, Func messageBoxExShow = null) { try { if (Directory.Exists(path)) Directory.Delete(path, true); } catch (Exception) { #if DEBUG throw; #else messageBoxExShow ??= MessageBox.Show; messageBoxExShow(string.Format("Not able to delete folder {0}, please go to the location and manually delete it", path)); #endif } } /// /// Attempts to delete a directory robustly with retry logic for locked files. /// This method tries to delete files individually with retries, then removes empty directories. /// Returns true if the directory was completely deleted, false if some files/folders remain. /// /// The directory path to delete /// Maximum number of retry attempts for locked files (default: 3) /// Delay in milliseconds between retries (default: 100ms) /// True if directory was fully deleted, false if some items remain public static bool TryDeleteDirectoryRobust(string path, int maxRetries = 3, int retryDelayMs = 100) { if (!Directory.Exists(path)) return true; bool fullyDeleted = true; try { // First, try to delete all files in the directory tree var files = Directory.GetFiles(path, "*", SearchOption.AllDirectories); foreach (var file in files) { bool fileDeleted = false; for (int attempt = 0; attempt <= maxRetries; attempt++) { try { // Remove read-only attribute if present var fileInfo = new FileInfo(file); if (fileInfo.Exists && fileInfo.IsReadOnly) { fileInfo.IsReadOnly = false; } File.Delete(file); fileDeleted = true; break; } catch (UnauthorizedAccessException) { // File is in use or access denied, wait and retry if (attempt < maxRetries) { System.Threading.Thread.Sleep(retryDelayMs); } } catch (IOException) { // File is in use, wait and retry if (attempt < maxRetries) { System.Threading.Thread.Sleep(retryDelayMs); } } catch { // Other exceptions, don't retry break; } } if (!fileDeleted) { fullyDeleted = false; } } // Then, try to delete all empty directories (from deepest to shallowest) var directories = Directory.GetDirectories(path, "*", SearchOption.AllDirectories) .OrderByDescending(d => d.Length) // Delete deeper directories first .ToArray(); foreach (var directory in directories) { try { if (Directory.Exists(directory) && !Directory.EnumerateFileSystemEntries(directory).Any()) { Directory.Delete(directory, false); } } catch { // If we can't delete an empty directory, mark as not fully deleted fullyDeleted = false; } } // Finally, try to delete the root directory itself try { if (Directory.Exists(path) && !Directory.EnumerateFileSystemEntries(path).Any()) { Directory.Delete(path, false); } else if (Directory.Exists(path)) { fullyDeleted = false; } } catch { fullyDeleted = false; } } catch { fullyDeleted = false; } return fullyDeleted; } /// /// Checks if a directory exists /// /// /// public static bool LocationExists(this string path) { return Directory.Exists(path); } /// /// Checks if a file exists /// /// /// public static bool FileExists(this string filePath) { return File.Exists(filePath); } /// /// Checks if a file or directory exists /// /// /// public static bool FileOrLocationExists(this string path) { return LocationExists(path) || FileExists(path); } /// /// Open a directory window (using the OS's default handler, usually explorer) /// /// /// public static void OpenPath(string fileOrFolderPath, Func messageBoxExShow = null) { var psi = new ProcessStartInfo { FileName = FileExplorerProgramName, UseShellExecute = true, Arguments = '"' + fileOrFolderPath + '"' }; try { if (LocationExists(fileOrFolderPath) || FileExists(fileOrFolderPath)) Process.Start(psi); } catch (Exception) { #if DEBUG throw; #else messageBoxExShow ??= MessageBox.Show; messageBoxExShow(string.Format("Unable to open the path {0}, please check if it exists", fileOrFolderPath)); #endif } } /// /// Open a file with associated application /// /// File path /// Working directory /// Open as Administrator /// public static void OpenFile(string filePath, string workingDir = "", bool asAdmin = false, Func messageBoxExShow = null) { var psi = new ProcessStartInfo { FileName = filePath, UseShellExecute = true, WorkingDirectory = workingDir, Verb = asAdmin ? "runas" : string.Empty }; try { if (FileExists(filePath)) Process.Start(psi); } catch (Exception) { #if DEBUG throw; #else messageBoxExShow ??= MessageBox.Show; messageBoxExShow(string.Format("Unable to open the path {0}, please check if it exists", filePath)); #endif } } /// /// This checks whether a given string is a zip file path. /// By default does not check if the zip file actually exist on disk, can do so by /// setting checkFileExists = true. /// public static bool IsZipFilePath(string querySearchString, bool checkFileExists = false) { if (IsLocationPathString(querySearchString) && querySearchString.Split('.').Last() == "zip") { if (checkFileExists) return FileExists(querySearchString); return true; } return false; } /// /// This checks whether a given string is a directory path or network location string. /// It does not check if location actually exists. /// public static bool IsLocationPathString(this string querySearchString) { if (string.IsNullOrEmpty(querySearchString) || querySearchString.Length < 3) return false; // // shared folder location, and not \\\location\ if (querySearchString.StartsWith(@"\\") && querySearchString[2] != '\\') return true; // c:\ if (char.IsLetter(querySearchString[0]) && querySearchString[1] == ':' && querySearchString[2] == '\\') { return querySearchString.Length == 3 || querySearchString[3] != '\\'; } return false; } /// /// Gets the previous level directory from a path string. /// Checks that previous level directory exists and returns it /// as a path string, or empty string if doesn't exist /// public static string GetPreviousExistingDirectory(Func locationExists, string path) { var index = path.LastIndexOf('\\'); if (index > 0 && index < (path.Length - 1)) { string previousDirectoryPath = path[..(index + 1)]; return locationExists(previousDirectoryPath) ? previousDirectoryPath : string.Empty; } else { return string.Empty; } } /// /// Returns the previous level directory if path incomplete (does not end with '\'). /// Does not check if previous level directory exists. /// Returns passed in string if is complete path /// public static string ReturnPreviousDirectoryIfIncompleteString(string path) { if (!path.EndsWith("\\")) { // not full path, get previous level directory string var indexOfSeparator = path.LastIndexOf('\\'); return path[..(indexOfSeparator + 1)]; } return path; } /// /// Returns if contains . Equal paths are not considered to be contained by default. /// From https://stackoverflow.com/a/66877016 /// /// Parent path /// Sub path /// If , when and are equal, returns /// public static bool PathContains(string parentPath, string subPath, bool allowEqual = false) { var rel = Path.GetRelativePath(parentPath.EnsureTrailingSlash(), subPath); return (rel != "." || allowEqual) && rel != ".." && !rel.StartsWith("../") && !rel.StartsWith(@"..\") && !Path.IsPathRooted(rel); } /// /// Returns path ended with "\" /// /// /// public static string EnsureTrailingSlash(this string path) { return path.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar; } /// /// Validates a directory, creating it if it doesn't exist /// /// public static void ValidateDirectory(string path) { if (!Directory.Exists(path)) { Directory.CreateDirectory(path); } } /// /// Validates a data directory, synchronizing it by ensuring all files from a bundled source directory exist in it. /// If files are missing or outdated, they are copied from the bundled directory to the data directory. /// /// /// public static void ValidateDataDirectory(string bundledDataDirectory, string dataDirectory) { if (!Directory.Exists(dataDirectory)) { Directory.CreateDirectory(dataDirectory); } foreach (var bundledDataPath in Directory.GetFiles(bundledDataDirectory)) { var data = Path.GetFileName(bundledDataPath); if (data == null) continue; var dataPath = Path.Combine(dataDirectory, data); if (!File.Exists(dataPath)) { File.Copy(bundledDataPath, dataPath); } else { var time1 = new FileInfo(bundledDataPath).LastWriteTimeUtc; var time2 = new FileInfo(dataPath).LastWriteTimeUtc; if (time1 != time2) { File.Copy(bundledDataPath, dataPath, true); } } } } } }