Данная статья будет полезна тем, кто сталкивался или, возможно, столкнется с такой ошибкой как PathToLongException, которая появляется когда мы выполняем какие-то операции над файлом, путь к которому очень длинный:
[PathTooLongException]: The specified path, file name, or both are too long. The fully qualified file name must be less than 260 characters, and the directory name must be less than 248 characters. |
Со своей стороны мы могли бы сказать заказчикам, что не получается сохранить файл с таким названием, т.к. это ограничение системы. Но наши заказчики, увидев данное сообщение, скорее всего скажут: "260 символов? Это же смешно! Увеличьте ограничение!"
.NET API очень сильно завязан на Windows API, поетому может показаться, что данную проблему не решить, т.к. сама Windows не дает оперировать с путями к файлам более 260 символов. Однако, Windows API все же предоставляет нам методы для обхода данного ограничения. Если перед путем к файлу поставить префикс "\\?\" и вызвать Unicode-версию функции Windows API, то можно использовать пути к файлам длиной до 32K символов.
Сейчас мы рассмотрим как работать с файлами, путь к которым более 260 символов, а потом поговорим о возможных проблемах и ограничениях.
Работа с длинными путями к файлам
Для примера создадим консольное приложение. Нам понадобятся такие пространства имен:
using System; using System.IO; using System.Text; |
Далее создадим две функции: для записи и чтения файла:
Функция WriteFile сохраняет файл с заданным именем и записывает в него информацию о количестве символов в его названии:
static void WriteFile(string fileName) { using (FileStream fs = new FileStream(fileName, FileMode.Create)) { string text = string.Format("FileName Length: {0} symbols", fileName.Length); byte[] bytes = Encoding.Default.GetBytes(text); fs.Write(bytes, 0, bytes.Length); } } |
Функция ReadFile читает и выводит на консоль содержимое файла:
static void ReadFile(string fileName) { using (FileStream fs = new FileStream(fileName, FileMode.Open)) { int c; while (fs.Position < fs.Length) { c = fs.ReadByte(); Console.Write((char)c); } } } |
В методе Main() попробуем вызвать эти функции:
static void Main(string[] args) { // имя файла 250 символов string fileName = @"C:\very_long_file_name_" + "0000003000000000400000000050000000006" + "0000000007000000000800000000090000000" + "1000000000001000000000200000000030000" + "0000040000000005000000000600000000070" + "0000000080000000009000000020000000000" + "010000000002000000000300000000040000000005"; Console.WriteLine("Write file..."); WriteFile(fileName); Console.WriteLine("Read file:"); ReadFile(fileName); Console.Read(); } |
Запустим программу на выполнение и проверим, что все хорошо работает:
Теперь модифицирум метод Main():
создадим на диске C: директорию с названием "very_long_file_name_in_this_directory" и немного изменим переменную, которая хранит имя файла. Сейчас путь к файлу будет состоять из 288 символов:
static void Main(string[] args) { string directoryName = @"C:\very_long_file_name_in_this_directory"; if (! Directory.Exists(directoryName)) { Directory.CreateDirectory(directoryName); } // имя файла 288 символов string fileName = directoryName + @"\very_long_file_name_" + "0000003000000000400000000050000000006" + "0000000007000000000800000000090000000" + "1000000000001000000000200000000030000" + "0000040000000005000000000600000000070" + "0000000080000000009000000020000000000" + "010000000002000000000300000000040000000005"; Console.WriteLine("Write file..."); WriteFile(fileName); Console.WriteLine("Read file:"); ReadFile(fileName); Console.Read(); } |
Запустив приложение на выполнение, мы получим сообщение об ошибке:
[PathTooLongException]: The specified path, file name, or both are too long. The fully qualified file name must be less than 260 characters, and the directory name must be less than 248 characters. |
Для решения данной проблемы мы воспользуемся Win32 API функкцией CreateFile.
Для начала добавим два пространства имен:
using System.Runtime.InteropServices; using Microsoft.Win32.SafeHandles; |
Далее добавим вспомогательные перечислители и битовые флаги (для того, чтобы не передавать в параметры метода непонятные числовые значения):
[Flags] public enum EFileAccess : uint { GenericRead = 0x80000000, GenericWrite = 0x40000000, GenericExecute = 0x20000000, GenericAll = 0x10000000, } [Flags] public enum EFileShare : uint { None = 0x00000000, Read = 0x00000001, Write = 0x00000002, Delete = 0x00000004, } public enum ECreationDisposition : uint { New = 1, CreateAlways = 2, OpenExisting = 3, OpenAlways = 4, TruncateExisting = 5, } [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 } |
Добавим описание Win32 API функции CreateFile, указав в параметре CharSet, что нам нужна Unicode версия этой функции:
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] internal static extern SafeFileHandle CreateFile ( string lpFileName, EFileAccess dwDesiredAccess, EFileShare dwShareMode, IntPtr lpSecurityAttributes, ECreationDisposition dwCreationDisposition, EFileAttributes dwFlagsAndAttributes, IntPtr hTemplateFile ); |
Функция CreateFile возвращает файловый описатель (file handle), который можно передать конструктору FileStream.
Запись в файл
Сейчас мы перепишем метод WriteFile так, чтобы он мог работать с длинными именами файлов:
static void WriteFile(string fileName) { string longFileName = @"\\?\" + fileName; // Создадим файл с правами на запись SafeFileHandle fileHandle = CreateFile ( longFileName, EFileAccess.GenericWrite, EFileShare.None, IntPtr.Zero, ECreationDisposition.CreateAlways, 0, IntPtr.Zero ); // Проверка на ошибки int lastWin32Error = Marshal.GetLastWin32Error(); if (fileHandle.IsInvalid) { throw new System.ComponentModel.Win32Exception(lastWin32Error); } using(FileStream fs = new FileStream(fileHandle, FileAccess.Write)) { string text = string.Format("FileName Length: {0} symbols", fileName.Length); byte[] bytes = Encoding.Default.GetBytes(text); fs.Write(bytes, 0, bytes.Length); } } |
Чтение из файла
Выполнив метод WriteFile у нас создастся файл с длинным именем. Правда теперь его нельзя будет открыть ни одним редактором, разве что это программа также будет использовать синтаксис "\\?\" для открытия файлов.
Наш метод ReadFile не исключение и в текущем состоянии он будет "кидать" все тот же "PathTooLongException".
Сейчас мы его перепишем аналогично методу WriteFile, изменив права записи на чтение:
static void ReadFile(string fileName) { string longFileName = @"\\?\" + fileName; // Создадим файл с правами на чтение SafeFileHandle fileHandle = CreateFile ( longFileName, EFileAccess.GenericRead, EFileShare.None, IntPtr.Zero, ECreationDisposition.OpenAlways, 0, IntPtr.Zero ); // Проверка на ошибки int lastWin32Error = Marshal.GetLastWin32Error(); if (fileHandle.IsInvalid) { throw new System.ComponentModel.Win32Exception(lastWin32Error); } using (FileStream fs = new FileStream(fileHandle, FileAccess.Read)) { int c; while (fs.Position < fs.Length) { c = fs.ReadByte(); Console.Write((char)c); } } } |
Запускаем приложение на выполнение и видим, что файл прочитался, при чем длина его имени равна 288 символов:
Удаление файла
Если мы попробуем удалить существующий файл вручную, например, из Проводника, то у нас этого не получится, система скажет, что файл не найден. У нас также не получится удалить файл программно используя клас File, например так: File.Delete(fileName);
Для того, чтобы удалить файл с длинным именем нам необходимо воспользоваться Win32 API функцией DeleteFile.
Добавим ее объявление, указав, что нам нужна Unicode версия:
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)] [return: MarshalAs(UnmanagedType.Bool)] internal static extern bool DeleteFile(string lpFileName); |
Теперь создадим метод Delete(), который добавит префикс "\\?\" к имени файла и вызовет только что добавленную функцию DeleteFile:
static void Delete(string fileName) { string longFileName = @"\\?\" + fileName; DeleteFile(longFileName); } |
Теперь, вызвав функцию Delete, можно удалять файлы с длинным названием.
Ограничения и недостатки данного подхода
Теперь мы знаем, как решить проблему длинных путей к файлам. Давайте рассмотрим, что мы можем потерять при использовании такого подхода.
Во-первых - безопасность. Префикс "\\?\" не только позволяет нам использовать длинные пути к файлам. Его применение включает минимальные права для модификации файловой системы. Этот префикс "выключает" нормализацию файлового имени, которая вызывается функциями Windows API, включая удаление концевых пробелов, использование синтаксиса "." и "..", конвертирование относительного пути в полный путь и т.п.
Во-вторых, не все Windows API функции поддерживают префикс "\\?\" . Например, если функция LoadLibrary получит путь к файлу более 260 символов, то будет сгенерировано исключение.
Еще одним фактором, который можно отнести к недостаткам, является совместимость с другими Windows-приложения и сама оболочка Windows, работающая только с путями к файлам меньше 260 символов. Это означает, что если ваше приложение поддерживает длинные пути, то совсем не обязательно, что другие программы смогут открыть ваши файлы. Тем более непосредственно из самой операционной системы такие файлы нельзя ни переименовать, ни скопировать, ни переместить и т.д.
Таким образом, если только ваше приложение будет работать со своими файлами, путь к которым очень длинный, то для решения такой задачи можно использовать описанный в этой статье подход. С другой стороны, если результатами работы вашего приложения должны пользоваться другие программы, то необходимо учесть ограничения данного подхода и по-возможности изменить логику сохранения файлов.