| // <copyright file="FileUtilities.cs" company="Selenium Committers"> |
| // Licensed to the Software Freedom Conservancy (SFC) under one |
| // or more contributor license agreements. See the NOTICE file |
| // distributed with this work for additional information |
| // regarding copyright ownership. The SFC licenses this file |
| // to you under the Apache License, Version 2.0 (the |
| // "License"); you may not use this file except in compliance |
| // with the License. You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, |
| // software distributed under the License is distributed on an |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| // KIND, either express or implied. See the License for the |
| // specific language governing permissions and limitations |
| // under the License. |
| // </copyright> |
| |
| using OpenQA.Selenium.Internal.Logging; |
| using System; |
| using System.Diagnostics.CodeAnalysis; |
| using System.Globalization; |
| using System.IO; |
| using System.Reflection; |
| |
| namespace OpenQA.Selenium.Internal; |
| |
| /// <summary> |
| /// Encapsulates methods for working with files. |
| /// </summary> |
| internal static class FileUtilities |
| { |
| private static readonly ILogger logger = Log.GetLogger(typeof(FileUtilities)); |
| |
| /// <summary> |
| /// Recursively copies a directory. |
| /// </summary> |
| /// <param name="sourceDirectory">The source directory to copy.</param> |
| /// <param name="destinationDirectory">The destination directory.</param> |
| /// <returns><see langword="true"/> if the copy is completed; otherwise <see langword="false"/>.</returns> |
| public static bool CopyDirectory(string sourceDirectory, string destinationDirectory) |
| { |
| bool copyComplete; |
| DirectoryInfo sourceDirectoryInfo = new DirectoryInfo(sourceDirectory); |
| DirectoryInfo destinationDirectoryInfo = new DirectoryInfo(destinationDirectory); |
| |
| if (sourceDirectoryInfo.Exists) |
| { |
| if (!destinationDirectoryInfo.Exists) |
| { |
| destinationDirectoryInfo.Create(); |
| } |
| |
| foreach (FileInfo fileEntry in sourceDirectoryInfo.GetFiles()) |
| { |
| fileEntry.CopyTo(Path.Combine(destinationDirectoryInfo.FullName, fileEntry.Name)); |
| } |
| |
| foreach (DirectoryInfo directoryEntry in sourceDirectoryInfo.GetDirectories()) |
| { |
| if (!CopyDirectory(directoryEntry.FullName, Path.Combine(destinationDirectoryInfo.FullName, directoryEntry.Name))) |
| { |
| copyComplete = false; |
| } |
| } |
| } |
| |
| copyComplete = true; |
| return copyComplete; |
| } |
| |
| /// <summary> |
| /// Recursively deletes a directory, retrying on error until a timeout. |
| /// </summary> |
| /// <param name="directoryToDelete">The directory to delete.</param> |
| /// <remarks>This method does not throw an exception if the delete fails.</remarks> |
| public static void DeleteDirectory(string? directoryToDelete) |
| { |
| int numberOfRetries = 0; |
| while (Directory.Exists(directoryToDelete) && numberOfRetries < 10) |
| { |
| try |
| { |
| Directory.Delete(directoryToDelete, true); |
| } |
| catch (IOException) |
| { |
| // If we hit an exception (like file still in use), wait a half second |
| // and try again. If we still hit an exception, go ahead and let it through. |
| System.Threading.Thread.Sleep(500); |
| } |
| catch (UnauthorizedAccessException) |
| { |
| // If we hit an exception (like file still in use), wait a half second |
| // and try again. If we still hit an exception, go ahead and let it through. |
| System.Threading.Thread.Sleep(500); |
| } |
| finally |
| { |
| numberOfRetries++; |
| } |
| } |
| |
| if (Directory.Exists(directoryToDelete)) |
| { |
| if (logger.IsEnabled(LogEventLevel.Trace)) |
| { |
| logger.Trace($"Unable to delete directory '{directoryToDelete}'"); |
| } |
| } |
| } |
| |
| /// <summary> |
| /// Searches for a file with the specified name. |
| /// </summary> |
| /// <param name="fileName">The name of the file to find.</param> |
| /// <returns>The full path to the directory where the file can be found, |
| /// or an empty string if the file does not exist in the locations searched.</returns> |
| /// <remarks> |
| /// This method looks first in the directory of the currently executing |
| /// assembly. If the specified file is not there, the method then looks in |
| /// each directory on the PATH environment variable, in order. |
| /// </remarks> |
| public static string FindFile(string fileName) |
| { |
| // Look first in the same directory as the executing assembly |
| string currentDirectory = GetCurrentDirectory(); |
| if (File.Exists(Path.Combine(currentDirectory, fileName))) |
| { |
| return currentDirectory; |
| } |
| |
| // If it's not in the same directory as the executing assembly, |
| // try looking in the system path. |
| string? systemPath = Environment.GetEnvironmentVariable("PATH"); |
| if (!string.IsNullOrEmpty(systemPath)) |
| { |
| string expandedPath = Environment.ExpandEnvironmentVariables(systemPath); |
| string[] directories = expandedPath.Split(Path.PathSeparator); |
| foreach (string directory in directories) |
| { |
| // N.B., if the directory in the path contains an invalid character, |
| // we will skip that directory, meaning no error will be thrown. This |
| // may be confusing to the user, so we might want to revisit this. |
| if (directory.IndexOfAny(Path.GetInvalidPathChars()) < 0) |
| { |
| if (File.Exists(Path.Combine(directory, fileName))) |
| { |
| currentDirectory = directory; |
| return currentDirectory; |
| } |
| } |
| } |
| } |
| |
| // Note that if it wasn't found on the system path, currentDirectory is still |
| // set to the same directory as the executing assembly. |
| return string.Empty; |
| } |
| |
| /// <summary> |
| /// Gets the directory of the currently executing assembly. |
| /// </summary> |
| /// <returns>The directory of the currently executing assembly.</returns> |
| public static string GetCurrentDirectory() |
| { |
| Assembly executingAssembly = typeof(FileUtilities).Assembly; |
| string? location = null; |
| |
| // Make sure not to call Path.GetDirectoryName if assembly location is null or empty |
| string assemblyLocation = executingAssembly.Location; |
| if (!string.IsNullOrEmpty(assemblyLocation)) |
| { |
| location = Path.GetDirectoryName(assemblyLocation); |
| } |
| |
| if (string.IsNullOrEmpty(location)) |
| { |
| // If there is no location information from the executing |
| // assembly, we will bail by using the current directory. |
| // Note this is inaccurate, because the working directory |
| // may not actually be the directory of the current assembly, |
| // especially if the WebDriver assembly was embedded as a |
| // resource. |
| location = Directory.GetCurrentDirectory(); |
| } |
| |
| string currentDirectory = location!; |
| |
| #if !NET8_0_OR_GREATER |
| // If we're shadow copying, get the directory from the codebase instead |
| if (AppDomain.CurrentDomain.ShadowCopyFiles) |
| { |
| var codeBase = executingAssembly.CodeBase; |
| |
| if (codeBase is not null) |
| { |
| currentDirectory = Path.GetDirectoryName(codeBase); |
| } |
| } |
| #endif |
| |
| return currentDirectory; |
| } |
| |
| /// <summary> |
| /// Generates the full path to a random directory name in the temporary directory, following a naming pattern.. |
| /// </summary> |
| /// <param name="directoryPattern">The pattern to use in creating the directory name, following standard |
| /// .NET string replacement tokens.</param> |
| /// <returns>The full path to the random directory name in the temporary directory.</returns> |
| public static string GenerateRandomTempDirectoryName([StringSyntax(StringSyntaxAttribute.CompositeFormat)] string directoryPattern) |
| { |
| string directoryName = string.Format(CultureInfo.InvariantCulture, directoryPattern, Guid.NewGuid().ToString("N")); |
| return Path.Combine(Path.GetTempPath(), directoryName); |
| } |
| } |