Любимая всеми ОС Windows с не менее любимой ФС NTFS может поддерживать пути длинной до 32К символов. Однако мало кто этим воспользовался, поскольку в обычной жизни DOS-пути (а ими являются все пути вида C:\Windows...) поддерживают всего лишь 260 символов. Частенько этого бывает мало. Скажем, необходимо восстановить инфраструктуру сайта, а пути там могут быть куда длиннее.
В таком случае хорошо бы воспользоваться обещанными 32К-длинными путями. Но вот незадача: при попытке сделать путь хотя бы немного длиннее дозволенного, мы схлопочем исключение:
The path is too long after being fully qualified. Make sure path is less than 260 characters. Для использования длинных путей с помощью WinAPI делают так:
1. Используют UNICODE-версии функций: CreateFileW, CreateDirectoryW и т.д.
2. Формируют путь в стиле NT: "\\?\C:\Windows..."
Еще одна проблема в том, что пути NT никак не модерируются системой: если обычные DOS пути можно писать как попало (C:/Windows\\\\\system32), и система спокойно съест такое, то для NT путей это недопустимо. То есть вся ответственность за корректные пути ложится на программиста.
Вернемся к дотнету. Он, конечно же, не предоставит нам выбора какими функциями пользоваться для работы с файлом. Придется использовать P/Invoke.
Проблема не нова, и решения можно поискать например здесь.
Я же предлагаю сконцентрироваться на устранении последствий применения P/Invoke, которое:
а. Поломает архитектуру
б. Сделает программу ОС-зависимой
Пункт (б) побороть никак не получится (кроме как использовать условную компиляцию). А вот архитектуру надо бы сохранить. Если у вас готова уже половина кода проекта, то переписывать куски с учетом использования нестандартного механизма работы с файлами совсем не хочется. Поэтому напишем класс, который будет совместим с обычным FileStream, но будет работать с длинными путями файловой системы.
Для сохранения архитектуры (конечно если она построена на принципах dependency injection) достаточно будет заменить фабрики, создающие объект файла или внести поправки в инициализацию DI контейнера.
Итак, ближе к делу. Сначала подготовим статический класс WinApi, в котором импортируем функции из kernel32.dll и определим константы. Также внимание надо уделить нормализации путей (ее сюда же добавим):using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using Microsoft.Win32.SafeHandles;
namespace WindowsSpecific
{
public static class WinApi
{
#region ECreationDisposition enum
public static string NormalizePath(string path)
{
const int bufferSize = 32767;
if (path.StartsWith(".") || !path.Contains(":"))
{
var stringBuilder = new StringBuilder {Length = bufferSize};
GetCurrentDirectory(bufferSize-1, stringBuilder);
path = stringBuilder + "/" + path.TrimStart(new[] { '.' });
}
path = path.Replace('/', '').Trim();
while (path.Contains(@""))
path = path.Replace(@"", @"");
return @"?" + path;
}
public const uint ErrorAlreadyExist = 0x000000b7;
public enum ECreationDisposition : uint
{
New = 1,
CreateAlways = 2,
OpenExisting = 3,
OpenAlways = 4,
TruncateExisting = 5,
}
#endregion
#region EFileAccess enum
[Flags]
public enum EFileAccess : uint
{
GenericRead = 0x80000000,
GenericWrite = 0x40000000,
GenericExecute = 0x20000000,
GenericAll = 0x10000000,
}
#endregion
#region EFileAttributes enum
[Flags]
public enum EFileAttributes : uint
{
Readonly = 0x00000001,
Hidden = 0x00000002,
System = 0x00000004,
Directory = 0x00000010,
Archive = 0x00000020,
Device = 0x00000040,
Normal = 0x00000080,
Temporary = 0x00000100,
SparseFile = 0x00000200,
ReparsePoint = 0x00000400,
Compressed = 0x00000800,
Offline = 0x00001000,
NotContentIndexed = 0x00002000,
Encrypted = 0x00004000,
Write_Through = 0x80000000,
Overlapped = 0x40000000,
NoBuffering = 0x20000000,
RandomAccess = 0x10000000,
SequentialScan = 0x08000000,
DeleteOnClose = 0x04000000,
BackupSemantics = 0x02000000,
PosixSemantics = 0x01000000,
OpenReparsePoint = 0x00200000,
OpenNoRecall = 0x00100000,
FirstPipeInstance = 0x00080000
}
#endregion
#region EFileShare enum
[Flags]
public enum EFileShare : uint
{
None = 0x00000000,
Read = 0x00000001,
Write = 0x00000002,
Delete = 0x00000004,
}
#endregion
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern SafeFileHandle CreateFile
(
string lpFileName,
EFileAccess dwDesiredAccess,
EFileShare dwShareMode,
IntPtr lpSecurityAttributes,
ECreationDisposition dwCreationDisposition,
EFileAttributes dwFlagsAndAttributes,
IntPtr hTemplateFile
);
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern bool CreateDirectory
(
string lpFileName,
IntPtr lpSecurityAttributes
);
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern bool CopyFile(
string lpExistingFileName,
string lpNewFileName,
bool bFailIfExists
);
[DllImport("kernel32.dll")]
public static extern uint GetCurrentDirectory(uint nBufferLength,
[Out] StringBuilder lpBuffer);
public static ECreationDisposition ECreationDispositionByFileMode(FileMode fileMode)
{
var winapiMode = ECreationDisposition.OpenAlways;
switch (fileMode)
{
case FileMode.CreateNew:
winapiMode = ECreationDisposition.New;
break;
case FileMode.Create:
winapiMode = ECreationDisposition.CreateAlways;
break;
case FileMode.Open:
winapiMode = ECreationDisposition.OpenExisting;
break;
case FileMode.OpenOrCreate:
winapiMode = ECreationDisposition.OpenAlways;
break;
case FileMode.Truncate:
winapiMode = ECreationDisposition.TruncateExisting;
break;
case FileMode.Append:
winapiMode = ECreationDisposition.OpenAlways;
break;
}
return winapiMode;
}
public static EFileAccess EFileAccessByFileAccess(FileAccess fileAccess)
{
var winapiAccess = EFileAccess.GenericAll;
switch (fileAccess)
{
case FileAccess.Read:
winapiAccess = EFileAccess.GenericRead;
break;
case FileAccess.Write:
winapiAccess = EFileAccess.GenericWrite;
break;
case FileAccess.ReadWrite:
winapiAccess = EFileAccess.GenericRead | EFileAccess.GenericWrite;
break;
}
return winapiAccess;
}
}
}
Теперь напишем класс, перегрузив FileStream:
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using Microsoft.Win32.SafeHandles;
namespace WindowsSpecific
{
public class LongPathFileStream : FileStream
{
private static SafeFileHandle Initialize(string fileName, FileMode fileMode, FileAccess fileAccess)
{
var longFileName = WinApi.NormalizePath(fileName);
var fileHandle = WinApi.CreateFile(longFileName,WinApi.EFileAccessByFileAccess(fileAccess),
WinApi.EFileShare.Read,IntPtr.Zero,WinApi.ECreationDispositionByFileMode(fileMode),0,IntPtr.Zero);
var lastWin32Error = Marshal.GetLastWin32Error();
if (fileHandle.IsInvalid)
throw new System.ComponentModel.Win32Exception(lastWin32Error);
return fileHandle;
}
public LongPathFileStream(string fileName)
:base(Initialize(fileName,FileMode.OpenOrCreate,FileAccess.ReadWrite),FileAccess.ReadWrite)
{
}
public LongPathFileStream(string fileName,FileAccess fileAccess)
:base(Initialize(fileName,FileMode.OpenOrCreate,fileAccess),fileAccess)
{
}
public LongPathFileStream(string fileName, FileMode fileMode, FileAccess fileAccess)
:base(Initialize(fileName,fileMode,fileAccess),FileAccess.ReadWrite)
{
if (fileMode == FileMode.Append)
AppendMode();
}
private void AppendMode()
{
Position = Length;
}
}
}
Вот и все! Теперь если вместо FileStream задействовать LongPathFileStream, то можно создавать и открывать файлы используя путь длинной до 32К символов.
Есть еще правда вот такая проблема. Если создать файл, путь к которому длиннее 260 символов, то прочитать или удалить такой файл можно лишь программами, которые умеют работать с длинными путями, а таких куда меньше чем хотелось бы. Либо воспользоваться кривыми приемами от Microsoft (читаем тут, параграф 4).
Для полноты картины нужно бы обернуть еще WinAPI функции для создания и удаления директории, поиск файлов. Но это в другой раз.
Читать дальше...