На самом деле есть два способа интерпретации исходного вопроса.
- Как определить, может ли конкретная файловая система сохранять чувствительность к регистру в именах файлов?
- Как определить, интерпретирует ли текущая операционная система имена файлов с учетом регистра при работе с определенной файловой системой.
Этот ответ основан на втором толковании, потому что я думаю, что именно об этом хотел знать ФП, а также то, что важно для большинства людей.
Следующий код основан на ответе M4N и Николаса Рауля и пытается создать действительно надежную реализацию, способную определить, обрабатывает ли операционная система имена файлов с учетом регистра в указанном каталоге (исключая подкаталоги, поскольку они могут быть смонтированы из другой файловой системы).
Он работает, создавая два новых файла подряд, один с прописными буквами, другой с прописными буквами. Файлы заблокированы исключительно и удаляются автоматически при закрытии. Это должно предотвратить любые негативные побочные эффекты, вызванные созданием файлов.
Конечно, эта реализация работает, только если указанный каталог существует, и текущий пользователь может создавать внутри него файлы.
Код написан для .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
, либо вне его.
Однако, по моему мнению, вероятность столкновения с этим состоянием гонки один раз, не говоря уже два раза подряд, астрономически мала.