пятница, 1 апреля 2011 г.

Длинные пути файловой системы в .NET

Любимая всеми ОС 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 функции для создания и удаления директории, поиск файлов. Но это в другой раз.



Читать дальше...