//****************************************************************************************************** // FilePath.cs - Gbtc // // Copyright © 2012, Grid Protection Alliance. All Rights Reserved. // // Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See // the NOTICE file distributed with this work for additional information regarding copyright ownership. // The GPA licenses this file to you under the Eclipse Public License -v 1.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.opensource.org/licenses/eclipse-1.0.php // // Unless agreed to in writing, the subject software distributed under the License is distributed on an // "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the // License for the specific language governing permissions and limitations. // // Code Modification History: // ---------------------------------------------------------------------------------------------------- // 02/05/2003 - J. Ritchie Carroll // Generated original version of source code. // 12/29/2005 - Pinal C. Patel // Migrated 2.0 version of source code from 1.1 source (GSF.Shared.FilePath). // 08/22/2007 - Darrell Zuercher // Edited code comments. // 09/19/2008 - J. Ritchie Carroll // Converted to C#. // 10/24/2008 - Pinal C. Patel // Edited code comments. // 12/17/2008 - F. Russell Robertson // Fixed bug in GetFilePatternRegularExpression(). // 06/30/2009 - Pinal C. Patel // Removed FilePathHasFileName() since the result was error prone. // 09/14/2009 - Stephen C. Wills // Added new header and license agreement. // 09/17/2009 - Pinal C. Patel // Modified GetAbsolutePath() to remove dependency on HttpContext.Current. // 04/19/2010 - Pinal C. Patel // Added GetApplicationDataFolder() method. // 04/21/2010 - Pinal C. Patel // Updated GetApplicationDataFolder() to include the company name if available. // 01/28/2011 - J. Ritchie Carroll // Added IsValidFileName function. // 02/14/2011 - J. Ritchie Carroll // Fixed bug in GetDirectoryName where last directory was being truncated as a file name. // 06/06/2011 - Stephen C. Wills // Fixed bug in GetFileName where path suffix was being removed before extracting the file name. // 07/29/2011 - Pinal C. Patel // Updated GetApplicationDataFolder() to use the TEMP directory for web applications. // 12/14/2012 - Starlynn Danyelle Gilliam // Modified Header. // //****************************************************************************************************** using System; using System.IO; using System.Runtime.InteropServices; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Web.Hosting; using GSF.Identity; using GSF.Interop; using GSF.Reflection; namespace GSF.IO { /// /// Contains File and Path manipulation methods. /// public static class FilePath { #region [ Members ] // Nested Types [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] private struct NETRESOURCE { public readonly int dwScope; public int dwType; public readonly int dwDisplayType; public readonly int dwUsage; public readonly string lpLocalName; public string lpRemoteName; public readonly string lpComment; public readonly string lpProvider; } // Constants private const int RESOURCETYPE_DISK = 0x1; // Delegates [DllImport("mpr.dll", EntryPoint = "WNetAddConnection2W", ExactSpelling = true, CharSet = CharSet.Unicode, SetLastError = true)] private static extern int WNetAddConnection2(ref NETRESOURCE lpNetResource, string lpPassword, string lpUsername, int dwFlags); [DllImport("mpr.dll", EntryPoint = "WNetCancelConnection2W", ExactSpelling = true, CharSet = CharSet.Unicode, SetLastError = true)] private static extern int WNetCancelConnection2(string lpName, int dwFlags, [MarshalAs(UnmanagedType.Bool)] bool fForce); // Fields private static readonly string s_fileNameCharPattern; #endregion #region [ Constructor ] static FilePath() { StringBuilder pattern = new StringBuilder(); // Defines a regular expression pattern for a valid file name character. We do this by // allowing any characters except those that would not be valid as part of a filename. // This essentially builds the "?" wildcard pattern match. pattern.Append("[^"); pattern.Append(Path.DirectorySeparatorChar.RegexEncode()); pattern.Append(Path.AltDirectorySeparatorChar.RegexEncode()); pattern.Append(Path.PathSeparator.RegexEncode()); pattern.Append(Path.VolumeSeparatorChar.RegexEncode()); foreach (char c in Path.GetInvalidPathChars()) { pattern.Append(c.RegexEncode()); } pattern.Append("]"); s_fileNameCharPattern = pattern.ToString(); } #endregion #region [ Methods ] #if !MONO /// /// Connects to a network share with the specified user's credentials. /// /// UNC share name to connect to. /// Username to use for connection. /// Password to use for connection. /// Domain name to use for connetion. Specify the computer name for local system accounts. public static void ConnectToNetworkShare(string sharename, string username, string password, string domain) { NETRESOURCE resource = new NETRESOURCE(); int result; resource.dwType = RESOURCETYPE_DISK; resource.lpRemoteName = sharename; if (domain.Length > 0) username = domain + "\\" + username; result = WNetAddConnection2(ref resource, password, username, 0); if (result != 0) throw new InvalidOperationException("Failed to connect to network share \"" + sharename + "\" as user " + username + " - " + WindowsApi.GetErrorMessage(result)); } /// /// Disconnects the specified network share. /// /// UNC share name to disconnect from. public static void DisconnectFromNetworkShare(string sharename) { DisconnectFromNetworkShare(sharename, true); } /// /// Disconnects the specified network share. /// /// UNC share name to disconnect from. /// true to force a disconnect; otherwise false. public static void DisconnectFromNetworkShare(string sharename, bool force) { int result = WNetCancelConnection2(sharename, 0, force); if (result != 0) throw new InvalidOperationException("Failed to disconnect from network share \"" + sharename + "\" - " + WindowsApi.GetErrorMessage(result)); } #endif /// /// Determines whether the specified file name matches any of the given file specs (wildcards are defined as '*' or '?' characters). /// /// The file specs used for matching the specified file name. /// The file name to be tested against the specified file specs for a match. /// true to specify a case-insensitive match; otherwise false. /// true if the specified file name matches any of the given file specs; otherwise false. public static bool IsFilePatternMatch(string[] fileSpecs, string fileName, bool ignoreCase) { bool found = false; foreach (string fileSpec in fileSpecs) { if (IsFilePatternMatch(fileSpec, fileName, ignoreCase)) { found = true; break; } } return found; } /// /// Determines whether the specified file name matches the given file spec (wildcards are defined as '*' or '?' characters). /// /// The file spec used for matching the specified file name. /// The file name to be tested against the specified file spec for a match. /// true to specify a case-insensitive match; otherwise false. /// true if the specified file name matches the given file spec; otherwise false. public static bool IsFilePatternMatch(string fileSpec, string fileName, bool ignoreCase) { return (new Regex(GetFilePatternRegularExpression(fileSpec), (ignoreCase ? RegexOptions.IgnoreCase : RegexOptions.None))).IsMatch(fileName); } /// /// Determines if the specified file name and path is valid. /// /// The file name with optional path to test for validity. /// true if the specified is a valid name; otherwise false. public static bool IsValidFileName(string filePath) { if (string.IsNullOrWhiteSpace(filePath)) return false; try { // Attempt to parse directory and file name - this will catch most issues string directory = Path.GetDirectoryName(filePath); string filename = Path.GetFileName(filePath); // Check for invalid directory characters if (!string.IsNullOrWhiteSpace(directory) && directory.IndexOfAny(Path.GetInvalidPathChars()) >= 0) return false; // Check for invalid file name characters if (string.IsNullOrWhiteSpace(filename) || filename.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0) return false; // Recurse in to check validity of each directory name if (!string.IsNullOrWhiteSpace(directory) && !string.IsNullOrWhiteSpace(Path.GetFileName(directory))) return IsValidFileName(directory); } catch { // If you can't parse a file name or directory, file path is definitely not valid return false; } return true; } /// /// Gets the file name and extension from the specified file path. /// /// The file path from which the file name and extension is to be obtained. /// File name and extension if the file path has it; otherwise empty string. public static string GetFileName(string filePath) { return Path.GetFileName(filePath); } /// /// Gets the extension from the specified file path. /// /// The file path from which the extension is to be obtained. /// File extension. public static string GetExtension(string filePath) { return Path.GetExtension(RemovePathSuffix(filePath)); } /// /// Gets the file name without extension from the specified file path. /// /// The file path from which the file name is to be obtained. /// File name without the extension if the file path has it; otherwise empty string. public static string GetFileNameWithoutExtension(string filePath) { return Path.GetFileNameWithoutExtension(RemovePathSuffix(filePath)); } /// /// Gets the size of the specified file. /// /// Name of file whose size is to be retrieved. /// The size of the specified file. public static long GetFileLength(string fileName) { try { return (new FileInfo(fileName)).Length; } catch { return -1; } } /// /// Gets a list of files under the specified path. Search wildcard pattern (c:\Data\*.dat) can be used for /// including only the files matching the pattern or path wildcard pattern (c:\Data\*\*.dat) to indicate the /// inclusion of files under all subdirectories in the list. /// /// The path for which a list of files is to be returned. /// A list of files under the given path. public static string[] GetFileList(string path) { string directory = GetDirectoryName(path); string filePattern = GetFileName(path); SearchOption options = SearchOption.TopDirectoryOnly; if (string.IsNullOrEmpty(filePattern)) { // No wildcard pattern was specified, so get a listing of all files. filePattern = "*.*"; } if (GetLastDirectoryName(directory) == "*") { // Path wildcard pattern is used to specify the option to include subdirectories. options = SearchOption.AllDirectories; directory = directory.Remove(directory.LastIndexOf("*")); } return Directory.GetFiles(directory, filePattern, options); } /// /// Gets a regular expression pattern that simulates wildcard matching for filenames (wildcards are defined as '*' or '?' characters). /// /// The file spec for which the regular expression pattern if to be generated. /// Regular expression pattern that simulates wildcard matching for filenames. public static string GetFilePatternRegularExpression(string fileSpec) { // Replaces wildcard file patterns with their equivalent regular expression. fileSpec = fileSpec.Replace("\\", "\\u005C"); // Backslash in Regex means special sequence. Here, we really want a backslash. fileSpec = fileSpec.Replace(".", "\\u002E"); // Dot in Regex means any character. Here, we really want a dot. fileSpec = fileSpec.Replace("?", s_fileNameCharPattern); fileSpec = fileSpec.Replace("*", "(" + s_fileNameCharPattern + ")*"); return "^" + fileSpec + "$"; } /// /// Gets the directory information from the specified file path. /// /// The file path from which the directory information is to be obtained. /// Directory information. public static string GetDirectoryName(string filePath) { return AddPathSuffix(Path.GetDirectoryName(filePath)); } /// /// Gets the last directory name from a file path. /// /// The file path from where the last directory name is to be retrieved. /// The last directory name from a file path. /// /// would return sub2 from c:\windows\sub2\filename.ext. /// public static string GetLastDirectoryName(string filePath) { // Test case should verify the following: // FilePath.GetLastDirectoryName(@"C:\Test\sub") == "Test" <-- sub assumed to be filename // FilePath.GetLastDirectoryName(@"C:\Test\sub\") == "sub" <-- sub assumed to be directory if (!string.IsNullOrEmpty(filePath)) { int index; char[] dirVolChars = { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar, Path.VolumeSeparatorChar }; // Remove file name and trailing directory seperator character from the file path. filePath = RemovePathSuffix(GetDirectoryName(filePath)); // Keep going through the file path until all directory seperator characters are removed. while ((index = filePath.IndexOfAny(dirVolChars)) > -1) { filePath = filePath.Substring(index + 1); } return filePath; } else { throw new ArgumentNullException("filePath"); } } /// /// Gets the absolute file path for the specified file name or relative file path. /// /// File name or relative file path. /// Absolute file path for the specified file name or relative file path. public static string GetAbsolutePath(string filePath) { if (!Path.IsPathRooted(filePath)) { // The specified path is a relative one since it is not rooted. switch (Common.GetApplicationType()) { // Prepends the application's root to the file path. case ApplicationType.Web: filePath = Path.Combine(HostingEnvironment.ApplicationPhysicalPath, filePath); break; case ApplicationType.WindowsCui: case ApplicationType.WindowsGui: filePath = Path.Combine(GetDirectoryName(AssemblyInfo.EntryAssembly.Location), filePath); break; } } return RemovePathSuffix(filePath); } /// /// Determines if the specified is contained with the current executable path. /// /// File name or relative file path. /// true if the specified is contained with the current executable path; otherwise false. public static bool InApplicationPath(string filePath) { return GetAbsolutePath(filePath).StartsWith(GetAbsolutePath(""), StringComparison.InvariantCultureIgnoreCase); } /// /// Gets the path to the folder where data related to the current application can be stored. /// /// Path to the folder where data related to the current application can be stored. public static string GetApplicationDataFolder() { string rootFolder; ApplicationType platform = Common.GetApplicationType(); if (platform == ApplicationType.Web) { // Treat web application special. if (HostingEnvironment.ApplicationVirtualPath == "/") rootFolder = Path.Combine(Path.GetTempPath(), HostingEnvironment.SiteName); else rootFolder = Path.Combine(Path.GetTempPath(), HostingEnvironment.ApplicationVirtualPath.Trim('/')); // Create a user folder if ID is available. string userID = UserInfo.RemoteUserID; if (string.IsNullOrEmpty(userID)) { // ID is not available. return rootFolder; } else { // Remove domain from ID. if (!userID.Contains("\\")) return Path.Combine(rootFolder, userID); else return Path.Combine(rootFolder, userID.Remove(0, userID.IndexOf('\\') + 1)); } } else { // Use entry assembly for everything else. rootFolder = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); if (string.IsNullOrEmpty(AssemblyInfo.EntryAssembly.Company)) return Path.Combine(rootFolder, AssemblyInfo.EntryAssembly.Name); else return Path.Combine(rootFolder, AssemblyInfo.EntryAssembly.Company + "\\" + AssemblyInfo.EntryAssembly.Name); } } /// /// Makes sure path is suffixed with standard . /// /// The file path to be suffixed. /// Suffixed path. public static string AddPathSuffix(string filePath) { if (string.IsNullOrEmpty(filePath)) { filePath = Path.DirectorySeparatorChar.ToString(); } else { char suffixChar = filePath[filePath.Length - 1]; if (suffixChar != Path.DirectorySeparatorChar && suffixChar != Path.AltDirectorySeparatorChar) filePath += Path.DirectorySeparatorChar; } return filePath; } /// /// Makes sure path is not suffixed with or . /// /// The file path to be unsuffixed. /// Unsuffixed path. public static string RemovePathSuffix(string filePath) { if (string.IsNullOrEmpty(filePath)) { filePath = ""; } else { char suffixChar = filePath[filePath.Length - 1]; while ((suffixChar == Path.DirectorySeparatorChar || suffixChar == Path.AltDirectorySeparatorChar) && filePath.Length > 0) { filePath = filePath.Substring(0, filePath.Length - 1); if (filePath.Length > 0) suffixChar = filePath[filePath.Length - 1]; } } return filePath; } /// /// Remove any path root present in the path. /// /// The file path whose root is to be removed. /// The path with the root removed if it was present. public static string DropPathRoot(string filePath) { // JRC: Changed this to the following more simple algorithm if (string.IsNullOrEmpty(filePath)) return ""; else return filePath.Remove(0, Path.GetPathRoot(filePath).Length); #region [ Original Code ] //string result = filePath; //if (!string.IsNullOrEmpty(filePath)) //{ // if ((filePath[0] == '\\') || (filePath[0] == '/')) // { // // UNC name ? // if ((filePath.Length > 1) && ((filePath[1] == '\\') || (filePath[1] == '/'))) // { // int index = 2; // int elements = 2; // // Scan for two separate elements \\machine\share\restofpath // while ((index <= filePath.Length) && // (((filePath[index] != '\\') && (filePath[index] != '/')) || (--elements > 0))) // { // index++; // } // index++; // if (index < filePath.Length) // { // result = filePath.Substring(index); // } // else // { // result = ""; // } // } // } // else if ((filePath.Length > 1) && (filePath[1] == ':')) // { // int dropCount = 2; // if ((filePath.Length > 2) && ((filePath[2] == '\\') || (filePath[2] == '/'))) // { // dropCount = 3; // } // result = result.Remove(0, dropCount); // } //} //return result; #endregion } /// /// Returns a file name, for display purposes, of the specified length using "..." to indicate a longer name. /// /// The file path to be trimmed. /// The maximum length of the trimmed file path. /// Trimmed file path. /// /// Minimum value for the parameter is 12. 12 will be used for any value /// specified as less than 12. /// public static string TrimFileName(string fileName, int length) { if (string.IsNullOrEmpty(fileName)) fileName = ""; else fileName = fileName.Trim(); if (length < 12) length = 12; if (fileName.Length > length) { string justName = GetFileName(fileName); if (justName.Length == fileName.Length) { // This is just a file name. Make sure extension shows. string justExtension = GetExtension(fileName); string trimName = GetFileNameWithoutExtension(fileName); if (trimName.Length > 8) { if (justExtension.Length > length - 8) justExtension = justExtension.Substring(0, length - 8); double offset = (length - justExtension.Length - 3) / 2.0D; return trimName.Substring(0, (int)(Math.Ceiling(offset))) + "..." + trimName.Substring((int)Math.Round(trimName.Length - Math.Floor(offset) + 1.0D)) + justExtension; } else { // We can not trim file names less than 8 with a "...", so we truncate long extension. return trimName + justExtension.Substring(0, length - trimName.Length); } } else if (justName.Length > length) { // File name alone exceeds length. Recurses into function without path. return TrimFileName(justName, length); } else { // File name contains path. Trims path before file name. string justFilePath = GetDirectoryName(fileName); int offset = length - justName.Length - 4; if (justFilePath.Length > offset && offset > 0) { return justFilePath.Substring(0, offset) + "...\\" + justName; } else { // Can not fit path. Trims file name. return TrimFileName(justName, length); } } } else { // Full file name fits within requested length. return fileName; } } /// /// Gets a unique file path for the given file by checking for name collisions and /// adding a sequence number to the end of the file name if there is a collision. /// /// The path to the original file before adding the sequence number. /// The unique path to the file. /// /// This method is designed to handle the case where the user wishes to create a file in a folder /// with a given name when there is a possibility that the name is already taken. Using this method, /// it is possible to create files with names in the following format: /// /// /// /// This method uses a linear search to find a unique file name, so it is suitable for situations where /// there are a small number of collisions for each file name. This will detect and fill gaps that can /// occur when files are deleted (for instance, if "File (1).ext" were deleted from the list above). /// public static string GetUniqueFilePath(string originalFilePath) { string uniqueFilePath = GetAbsolutePath(originalFilePath); string directory = GetDirectoryName(uniqueFilePath); string originalFileRoot = GetFileNameWithoutExtension(uniqueFilePath); string fileExtension = GetExtension(uniqueFilePath); int i = 1; while (File.Exists(uniqueFilePath)) { uniqueFilePath = Path.Combine(directory, string.Format("{0} ({1}){2}", originalFileRoot, i, fileExtension)); i++; } return uniqueFilePath; } /// /// Gets a unique file path for the given file by checking for name collisions and /// adding a sequence number to the end of the file name if there is a collision. /// /// The path to the original file before adding the sequence number. /// The unique path to the file. /// /// This method is designed to handle the case where the user wishes to create a file in a folder /// with a given name when there is a possibility that the name is already taken. Using this method, /// it is possible to create files with names in the following format: /// /// /// /// This method uses a binary search to find a unique file name, so it is suitable for situations where /// a large number of files will be created with the same file name, and the next unique file name needs /// to be found relatively quickly. It will not always detect gaps in the sequence numbers that can occur /// when files are deleted (for instance, if "File (1).ext" were deleted from the list above). /// public static string GetUniqueFilePathWithBinarySearch(string originalFilePath) { string uniqueFilePath = GetAbsolutePath(originalFilePath); string directory = GetDirectoryName(uniqueFilePath); string originalFileRoot = GetFileNameWithoutExtension(uniqueFilePath); string fileExtension = GetExtension(uniqueFilePath); int i = 1; int j = 1; int k = 1; if (!File.Exists(uniqueFilePath)) return uniqueFilePath; while (File.Exists(uniqueFilePath)) { uniqueFilePath = Path.Combine(directory, string.Format("{0} ({1}){2}", originalFileRoot, i, fileExtension)); j = k; k = i; i *= 2; } while (j < k) { i = (j + k) / 2; uniqueFilePath = Path.Combine(directory, string.Format("{0} ({1}){2}", originalFileRoot, i, fileExtension)); if (File.Exists(uniqueFilePath)) j = i + 1; else k = i; } return Path.Combine(directory, string.Format("{0} ({1}){2}", originalFileRoot, k, fileExtension)); } /// /// Waits for the default duration (5 seconds) for read access on a file. /// /// The name of the file to wait for to obtain read access. public static void WaitForReadLock(string fileName) { WaitForReadLock(fileName, 5); } /// /// Waits for read access on a file for the specified number of seconds. /// /// The name of the file to wait for to obtain read access. /// The time to wait for in seconds to obtain read access on a file. Set to zero to wait infinitely. public static void WaitForReadLock(string fileName, double secondsToWait) { if (!File.Exists(fileName)) throw new FileNotFoundException("Could not test file lock for \"" + fileName + "\", file does not exist", fileName); // Keeps trying for a file lock. FileStream targetFile = null; double startTime = Common.SystemTimer; while (true) { try { targetFile = File.OpenRead(fileName); targetFile.Close(); break; } catch { // Keeps trying to open the file. } if ((object)targetFile != null) { try { targetFile.Close(); } catch { } targetFile = null; } if (secondsToWait > 0) { if (Common.SystemTimer > startTime + secondsToWait) throw new IOException("Could not open \"" + fileName + "\" for read access, tried for " + secondsToWait + " seconds"); } // Yields to all other system threads. Thread.Sleep(250); } } /// /// Waits for the default duration (5 seconds) for write access on a file. /// /// The name of the file to wait for to obtain write access. public static void WaitForWriteLock(string fileName) { WaitForWriteLock(fileName, 5); } /// /// Waits for write access on a file for the specified number of seconds. /// /// The name of the file to wait for to obtain write access. /// The time to wait for in seconds to obtain write access on a file. Set to zero to wait infinitely. public static void WaitForWriteLock(string fileName, double secondsToWait) { if (!File.Exists(fileName)) throw new FileNotFoundException("Could not test file lock for \"" + fileName + "\", file does not exist", fileName); // Keeps trying for a file lock. FileStream targetFile = null; double startTime = Common.SystemTimer; while (true) { try { targetFile = File.OpenWrite(fileName); targetFile.Close(); break; } catch { // Keeps trying to open the file. } if ((object)targetFile != null) { try { targetFile.Close(); } catch { } targetFile = null; } if (secondsToWait > 0) { if (Common.SystemTimer > startTime + secondsToWait) throw new IOException("Could not open \"" + fileName + "\" for write access, tried for " + secondsToWait + " seconds"); } // Yields to all other system threads. Thread.Sleep(250); } } /// /// Waits for the default duration (5 seconds) for a file to exist. /// /// The name of the file to wait for until it is created. public static void WaitTillExists(string fileName) { WaitTillExists(fileName, 5); } /// /// Waits for a file to exist for the specified number of seconds. /// /// The name of the file to wait for until it is created. /// The time to wait for in seconds for the file to be created. Set to zero to wait infinitely. public static void WaitTillExists(string fileName, double secondsToWait) { // Keeps waiting for a file to be created. double startTime = Common.SystemTimer; while (!File.Exists(fileName)) { if (secondsToWait > 0) { if (Common.SystemTimer > startTime + secondsToWait) throw new IOException("Waited for \"" + fileName + "\" to exist for " + secondsToWait + " seconds, but it was never created"); } // Yields to all other system threads. Thread.Sleep(250); } } #endregion } }