.net 是否有办法确定本地文件系统是否区分大小写?
10 回答
您可以在临时文件夹中创建一个文件(使用小写文件名),然后检查该文件是否存在(使用大写文件名),例如:
string file = Path.GetTempPath() + Guid.NewGuid().ToString().ToLower();
File.CreateText(file).Close();
bool isCaseInsensitive = File.Exists(file.ToUpper());
File.Delete(file);
请记住,您可能有多个具有不同大小写规则的文件系统。例如,根文件系统可能区分大小写,但您可以将不区分大小写的文件系统(例如,带有 FAT 文件系统的 U 盘)安装在某处。因此,如果您进行此类检查,请确保将它们放在您要访问的目录中。
此外,如果用户将数据从区分大小写的文件系统复制到不区分大小写的文件系统怎么办?如果您的文件仅按大小写不同,其中一个会覆盖另一个,从而导致数据丢失。在另一个方向复制时,您也可能会遇到问题,例如,如果文件 A 包含对文件“b”的引用,但文件实际上名为“B”。这适用于原始不区分大小写的文件系统,但不适用于区分大小写的系统。
因此,如果可以,我建议您避免取决于文件系统是否区分大小写。不要生成仅大小写不同的文件名,使用标准文件选择器对话框,准备好大小写可能会改变等。
.NET 类库中没有这样的函数。
但是,您可以推出自己的:尝试使用小写名称创建文件,然后尝试使用其名称的大写版本打开它。可能有可能改进这种方法,但你明白了。
编辑:您实际上可以只获取根目录中的第一个文件,然后检查 filename.ToLower() 和 filename.ToUpper() 是否存在。不幸的是,同一文件的大写和小写变体很可能同时存在,因此您应该比较小写和大写变体的 FileInfo.Name 属性,看看它们是否确实相同。这不需要写入磁盘。
显然,如果卷上根本没有文件,这将失败。在这种情况下,只需退回到第一个选项(参见 Martin 的实现答案)。
它不是一个 .NET 函数,但来自 Windows API 的 GetVolumeInformation 和 GetVolumeInformationByHandleW 函数将执行您想要的操作(请参阅 yje lpFileSystemFlags 参数。
这个启发式怎么样?
public static bool IsCaseSensitiveFileSystem() {
var tmp = Path.GetTempPath();
return !Directory.Exists(tmp.ToUpper()) || !Directory.Exists(tmp.ToLower());
}
这是不使用临时文件的方法:
using System;
using System.Runtime.InteropServices;
static bool IsCaseSensitive()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ||
RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) // HFS+ (the Mac file-system) is usually configured to be case insensitive.
{
return false;
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
return true;
}
else if (Environment.OSVersion.Platform == PlatformID.Unix)
{
return true;
}
else
{
// A default.
return false;
}
}
相反,它包含有关操作环境的根深蒂固的知识。
作为 NuGet 包随时可用,适用于所有 .NET 4.0+ 并定期更新:https ://github.com/gapotchenko/Gapotchenko.FX/tree/master/Source/Gapotchenko.FX.IO#iscasesensitive
尝试创建一个全小写的临时文件,然后使用大写检查它是否存在。
实际上有两种方法可以解释原始问题。
- 如何确定特定文件系统是否能够在文件名中保持区分大小写?
- 如何确定当前操作系统在使用特定文件系统时是否区分大小写解释文件名。
这个答案基于第二种解释,因为我认为这是 OP 想知道的,也是对大多数人来说重要的。
以下代码大致基于 M4N 和 Nicolas Raoul 的回答,并尝试创建一个真正强大的实现,该实现能够确定操作系统是否在指定目录(不包括子目录,因为这些可能是从另一个文件系统挂载)。
它通过连续创建两个新文件来工作,一个是小写的,另一个是大写的。这些文件被独占锁定,并在关闭时自动删除。这应该避免由创建文件引起的任何负面影响。当然,这个实现只有在指定的目录存在并且当前用户能够在其中创建文件的情况下才有效。
该代码是为 .NET Framework 4.0 和 C# 7.2(或更高版本)编写的。
using System;
using System.IO;
using System.Reflection;
/// <summary>
/// Check whether the operating system handles file names case-sensitive in the specified directory.
/// </summary>
/// <param name="directoryPath">The path to the directory to check.</param>
/// <returns>A value indicating whether the operating system handles file names case-sensitive in the specified directory.</returns>
/// <exception cref="ArgumentNullException"><paramref name="directoryPath"/> is null.</exception>
/// <exception cref="ArgumentException"><paramref name="directoryPath"/> contains one or more invalid characters.</exception>
/// <exception cref="DirectoryNotFoundException">The specified directory does not exist.</exception>
/// <exception cref="UnauthorizedAccessException">The current user has no write permission to the specified directory.</exception>
private static bool IsFileSystemCaseSensitive(string directoryPath)
{
if (directoryPath == null)
{
throw new ArgumentNullException(nameof(directoryPath));
}
while (true)
{
string fileNameLower = ".cstest." + Guid.NewGuid().ToString();
string fileNameUpper = fileNameLower.ToUpperInvariant();
string filePathLower = Path.Combine(directoryPath, fileNameLower);
string filePathUpper = Path.Combine(directoryPath, fileNameUpper);
FileStream fileStreamLower = null;
FileStream fileStreamUpper = null;
try
{
try
{
// Try to create filePathUpper to ensure a unique non-existing file.
fileStreamUpper = new FileStream(filePathUpper, FileMode.CreateNew, FileAccess.Write, FileShare.None, bufferSize: 4096, FileOptions.DeleteOnClose);
// After ensuring that it didn't exist before, filePathUpper must be closed/deleted again to ensure correct opening of filePathLower, regardless of the case-sensitivity of the file system.
// On case-sensitive file systems there is a tiny chance for a race condition, where another process could create filePathUpper between closing/deleting it here and newly creating it after filePathLower.
// This method would then incorrectly indicate a case-insensitive file system.
fileStreamUpper.Dispose();
}
catch (IOException ioException) when (IsErrorFileExists(ioException))
{
// filePathUpper already exists, try another file name
continue;
}
try
{
fileStreamLower = new FileStream(filePathLower, FileMode.CreateNew, FileAccess.Write, FileShare.None, bufferSize: 4096, FileOptions.DeleteOnClose);
}
catch (IOException ioException) when (IsErrorFileExists(ioException))
{
// filePathLower already exists, try another file name
continue;
}
try
{
fileStreamUpper = new FileStream(filePathUpper, FileMode.CreateNew, FileAccess.Write, FileShare.None, bufferSize: 4096, FileOptions.DeleteOnClose);
// filePathUpper does not exist, this indicates case-sensitivity
return true;
}
catch (IOException ioException) when (IsErrorFileExists(ioException))
{
// fileNameUpper already exists, this indicates case-insensitivity
return false;
}
}
finally
{
fileStreamLower?.Dispose();
fileStreamUpper?.Dispose();
}
}
}
/// <summary>
/// Determines whether the specified <see cref="IOException"/> indicates a "file exists" error.
/// </summary>
/// <param name="ioException">The <see cref="IOException"/> to check.</param>
/// <returns>A value indicating whether the specified <see cref="IOException"/> indicates a "file exists" error.</returns>
private static bool IsErrorFileExists(IOException ioException)
{
// https://referencesource.microsoft.com/mscorlib/microsoft/win32/win32native.cs.html#dd35d7f626262141
const int ERROR_FILE_EXISTS = 0x50;
// The Exception.HResult property's get accessor is protected before .NET 4.5, need to get its value via reflection.
int hresult = (int)typeof(Exception)
.GetProperty("HResult", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
.GetValue(ioException, index: null);
// https://referencesource.microsoft.com/mscorlib/microsoft/win32/win32native.cs.html#9f6ca3226ff8f9ba
return hresult == unchecked((int)0x80070000 | ERROR_FILE_EXISTS);
}
如您所见,竞争条件极有可能导致误报。如果这种竞争条件是您真正担心的事情,我建议您在结果为假时再次进行检查,无论是在IsFileSystemCaseSensitive
方法内部还是外部。但是,在我看来,遇到这种竞态条件一次,更不用说连续两次的概率是天文数字的小了。
/// <summary>
/// Check whether the operating system is case-sensitive.
/// For instance on Linux you can have two files/folders called
//// "test" and "TEST", but on Windows the two can not coexist.
/// This method does not extend to mounted filesystems, which might have different properties.
/// </summary>
/// <returns>true if the operating system is case-sensitive</returns>
public static bool IsFileSystemCaseSensitive()
{
// Actually try.
string file = Path.GetTempPath() + Guid.NewGuid().ToString().ToLower() + "test";
File.CreateText(file).Close();
bool result = ! File.Exists(file.ToUpper());
File.Delete(file);
return result;
}
基于 M4N 的回答,有以下变化:
- 静态名称,以便我们确定它包含一个字母而不仅仅是数字。
- 也许更具可读性?
- 包裹在一个方法中。
- 文档。
更好的策略是将路径作为参数,并在同一个文件系统上创建文件,但在那里写入可能会产生意想不到的后果。
我调用作弊:
Path.DirectorySeparatorChar == '\\' ? "I'm insensitive" : "I'm probably sensitive"