Создание своей файловой системы для Windows

Создание файловой системы для Windows

В Linux, как известно, многие вещи реализованы как файлы в файловой системе. А если и не реализованы, то их можно реализовать самому с помощью FUSE, мы уже об этом писали в статье «Создание файловой системы на FUSE и Swift». В Windows это реализовать труднее, но если все же очень хочется смонтировать что-то как файловую систему, то это возможно. В этой статье я покажу, как этого добиться, используя C# и библиотеку Dokan.

Если вы уже знакомы с утилитой CyberSafe Top Secret, то вы, наверное, тоже столкнулись с тем, что добавлять файлы в контейнер не совсем удобно. Совсем другое дело — VeraCrypt: монтируется локальный диск, и файлы шифруются на лету. Именно так будет работать и наш проект.

Теория

Каждый раз, когда вы открываете папку «Компьютер», файловый менеджер отправляет запрос ядру с просьбой сказать, какие есть диски. Как происходит общение с драйвером? Через диспетчер ввода-вывода. Любое приложение может отправить ему пакет с запросом (IRP, I/O Request Packet) и информацией, кому он предназначен. Диспетчер принимает этот запрос и передает его нужному драйверу.

создание операционной системы

Получив список занятых букв, файловый менеджер последовательно опрашивает систему о том, какие метки тома у этих дисков, сколько на них осталось места, какая файловая система и так далее. Каждый такой запрос передается первому драйверу в цепочке. Если тот не знает, что ответить, он спрашивает следующий, тот в свою очередь — дальше. И так, пока какой-либо из них не ответит, что нужные данные найдены (или не найдены). На самом деле эта схема работает чуток посложнее, но для общего понимания этого достаточно.

Передача данных между драйверами по цепочке позволяет существовать руткитам и прочим вредоносам.

Любой драйвер средствами все того же диспетчера ввода-вывода может что-нибудь спросить у любого приложения, работающего в user-mode, что и используется в драйвере FUSE.

FUSE

Хоть мы и не будем использовать FUSE, в двух словах расскажу, что это такое. FUSE работает одним из драйверов в цепочке и позволяет быстро создать свою файловую систему без возни с драйвером, а еще такую файловую систему могут монтировать пользователи без прав рута.

Результат создания своего драйвера ФС
Результат создания своего драйвера файловой системы

FUSE не является драйвером файловой системы и не отвечает на запросы самостоятельно, а передает их пользовательскому приложению, которое и отвечает на запрос. Ответ приложения отправляется обратно в ядро, а оттуда — приложению, которое запросило информацию.

Dokan

В теории существует версия FUSE для Windows, однако заставить ее работать мне не удалось. Возможно, это было бы само по себе интересным опытом, но я избрал другой путь.

Есть такой проект — Dokan. По сути, это тот же FUSE, но с кучей приятных дополнительных функций. Во-первых, он ни разу за время его использования у меня не выдал ни одного синего экрана смерти. Во-вторых, есть библиотеки, которые позволяют работать с ним из самых разных языков, включая Delphi, Ruby, C# и Java (их вы найдете на GitHub по ссылке выше). И в-третьих, разобраться с ним почти так же просто, как и с FUSE. Так что будем использовать его, библиотеку под C# и немного фантазии.

От изначального проекта Dokan сейчас осталось очень мало. После версии 0.6.0 появился серьезно доработанный форк под названием Dokany. Теперь жив только Dokany, и, соответственно, мы будем использовать его. В дальнейшем, говоря о Dokan, я буду подразумевать именно Dokany.

Подготовка

Чтобы использовать Dokan, нам потребуется драйвер. К нашему счастью, есть уже готовые собранные драйверы, которые нужно всего лишь установить. Тут есть три способа.

  • Первый — воспользоваться автоматическим установщиком.
  • Второй — скачать собранные бинарники (они уже подписаны) и встроить их в свой установщик.
  • Ну и третий — скачать исходный код, благо он открыт (часть проекта распространяется по лицензии LGPLv3, часть — по MIT), и собрать все самостоятельно. Плюс такого подхода в том, что мы можем подписать готовый драйвер своей подписью, но на этом плюсы заканчиваются.

Я выбрал первый вариант. Скачать установщик вы можете здесь. Мастер в конце попросит перезагрузить компьютер, что вы и должны сделать. Если после перезагрузки вы увидите драйвер dokan1.sys, то все сделано правильно. Если нет — можно попробовать установить вручную.

Загруженный драйвер dokan1.sys
Загруженный драйвер dokan1.sys

Чтобы установить вручную, необходимо скачать более объемный файл. Кроме драйверов, он содержит и нужные вам библиотеки (если вы знаете C++), так что не спешите удалять его после установки.

Нас же сейчас интересует папка x64 (у вас ведь 64 бита?). В ней — набор папок, как на картинке.

Содержимое папки x64
Содержимое папки x64

У меня Windows 8.1, так что иду в соответствующую папку (рекомендую Release) и, кликнув по inf-файлу правой кнопкой мышки, выбираю «Установить». Подтверждаю проверку UAC и жду окончания процесса, после чего перезагружаю машину.

Теперь установка должна пройти успешно. Если что-то не получилось — убедитесь, что устанвавливаете ту версию драйвера.

В этом может помочь утилита DriverView.

Кроме библиотеки Dokan, нам еще понадобится Visual Studio. Недавно вышла новая версия 2019, так что, даже если у вас уже установлена, очень советую обновиться. С приготовлениями закончили, переходим к кодингу.

Любые вмешательства в файловую систему, в том числе создание своей файловой системы, могут повредить или уничтожить ваши данные. Все описанное в статье вы повторяете на свой страх и риск. Ни автор статьи, ни редакция сайта tech-geek.ru не несут ответственности за ваши действия. Все операции рекомендуется предварительно выполнять в виртуальной машине.

Кодинг

Итак, открываем Visual Studio и создаем новый проект типа Console App (.NET Framework). На скриншоте видно, что целевой фреймворк — 4.5.2, но минимально поддерживаемый — 4.0. Так что, если ваш компьютер не поддерживает 4.5.2, вы знаете, что делать.

создать операционную систему

Проект создан, и теперь нашему взору предстала заглушка метода Main. Вы ведь установили NuGet вместе со «Студией»? Если нет, устанавливайте. Оттуда мы ставим пакет DokanNet (Tools → NuGet Package Manager → Manage NuGet Packages for Solution). Любители командной строки могут открыть PowerShell-консоль NuGet (Tools → NuGet Package Manager → Package Manager Console) и выполнить Install-Package DokanNet.

создать операционную систему

Чтобы создать свою файловую систему, нам нужен класс, реализующий IDokanOperations. Создаем новый класс ( Ctrl+ Shift+ A) и добавляем туда using DokanDet;. Наш класс должен реализовывать интерфейс IDokanOperations, так что исправляем class XakepFSClass на class XakepFSClass : IDokanOperations.

создать операционную систему для windows

Как вы видите, в 10-й строке ошибка. Конечно, мы же унаследовали кучу методов от интерфейса, но не реализовали их. Я знаю, вы не хотите объявлять каждый метод вручную, поэтому поставьте курсор на неугодное выражение ( IDokanOperations в 10-й строке) и нажмите Alt-Enter. В появившемся меню выберите Implement interface.

сделать операционную систему windows

Теперь порядок! Но все методы выкидывают исключение NotImplementedException, что нам никак не подходит. Давайте реализуем Hello World, а затем — файловую систему, хранящую все данные в JSON.

HelloWorldFS

Поскольку это просто Hello World, мне не хочется изменять файл, который мы только что создали. Сделаем его копию, переименуем для лучшего восприятия (для переименования выберите файл в правой панели и нажмите F2). Теперь откроем наш новый класс и переименуем и его, а то компилятор не поймет наши фокусы. У вас должно получиться как на скриншоте.

сделать операционную систему windows

Если попробовать запустить сейчас, то ничего не выйдет. Файловую систему сначала нужно смонтировать. Давайте добавим в Program.cs такую строчку:

Это смонтирует нашу файловую систему на диск M:. У вызова Mount есть дополнительные параметры, которые я рассмотрю в конце статьи.

Если запустить теперь, то спустя несколько секунд мы получим ошибку, гласящую, что метод public NtStatus CreateFile, который вызывается при каждой попытке записать или прочитать файл, не реализован, и это нужно исправлять. Вообще, для корректной работы файловой системы необязательно реализовывать все функции, но некоторые придется. Список таких функций:

  • CreateFile;
  • Mounted;
  • GetFileInformation;
  • GetDiskFreeSpace;
  • Cleanup;
  • CloseFile;
  • GetVolumeInformation;
  • FindFilesWithPattern;
  • DeleteDirectory;
  • DeleteFile;
  • Unmounted.

Теперь пройдемся по реализации. Функции Cleanup и CloseFile не несут решительно никакой полезной нагрузки, так что просто удаляем из них весь код. GetDiskFreeSpace (как ни странно!) отвечает за отображение свободного и занятого места на диске. Я сделал просто возврат статических значений, однако в реальной ситуации (мы ее рассмотрим позже) уже будем вычислять и отдавать актуальные данные.

сделать файловую систему windows

Функции CreateFile, DeleteDirectory, DeleteFile, Mounted и Unmounted нас тоже пока что не интересуют. Во всех них мы напишем просто return NtStatus.Success;, чтобы не выпадали ошибки. А вот от GetFileInformation, GetVolumeInformation и FindFilesWithPattern мы так просто не отвяжемся. Они требуют результаты через переменные с модификатором out, так что мы даже не сможем их скомпилировать. Чтобы заставить замолчать и их, придется поместить следующий код:

GetFileInformation

FindFilesWithPattern

GetVolumeInformation

Эта последняя функция интересна тем, что мы можем сами задать любую метку тома (строка 1), название файловой системы (строка 3) и максимальную длину пути (строка 4). Я не стал ломать традиции и оставил максимальную длину пути равной 256 символам, но это ограничение мы уберем, когда дойдем до более реального примера.

Теперь можно все сохранить и запустить. Если вы все сделали правильно, то красивое черное окошко вывалит вам кучу текста, как на скриншоте, а в папке «Компьютер» появится новый локальный диск!

сделать файловую систему windows

Результат запуска
файловая система windows dokan

У новоиспеченного диска даже можно просмотреть свойства (см. скриншот ниже), но не пытайтесь его открыть, так как получите NotImplementedException в ReadFile.

файловая система windows dokan

Чтобы исправить эту досадную проблему, придется реализовывать не только метод ReadFile (это само собой), но и FindFiles, FindFilesWithPattern, FlushFileBuffers, GetFileSecurity и GetFileInformation. Поскольку это всего лишь Hello World, давайте будем возвращать только один текстовый файл с содержимым "Hello World from HelloWorldFS!\r\nThis is just a test file.".

В FindFiles напишем вот такую заглушку:

В FindFilesWithPattern напишем return FindFiles(null, out files, null);, а в FlushFileBuffersreturn NtStatus.Success;.

GetFileInformation

GetFileSecurity

ReadFile

После этих ухишрений программу можно запускать и открывать наш новый диск. Теперь там лежит файл HelloWorld.txt. Он даже открывается, хотя мы и не сможем сохранить его, если изменим ( WriteFile выдаст NotImplementedException). Это вы сможете реализовать сами, так как сохранение данных не входит в задачи нашего простого примера.

Наш Hello World работает!
Наш Hello World работает!

Если у вас что-то не получилось — посмотрите готовый код HelloWorldFS на Pastebin.

XakepFS

Переходим к «боевому» проекту! Для интереса будем не просто сохранять файлы, а превращать их в JSON. Зачем хранить файлы в JSON? Ну например, чтобы сохранить атрибуты и метаданные при сохранении в каком-нибудь сервисе, который их не поддерживает. Для примера возьмем GitHub, но в реальности таких сервисов масса.

Как правило для сохранения метаданных файлов в такой системе пришлось бы паковать все в архив или вытаскивать метаданные и хранить отдельно (и потом возвращать их на место — тот еще гемор). Нам же будет достаточно смонтировать нашу папку на какой-нибудь диск.

Работа с JSON в .NET

Думаю, рассказывать, что такое JSON, излишне, поэтому поговорим о том, как с ним работать в .NET. Как вы (наверное) знаете, в стандартной библиотеке нет средств для работы с ним. Конечно же, существует стороннее решение, оно называется Json.NET. Плюс этой библиотеки в том, что она умеет запаковывать в JSON и распаковывать из него все, что угодно, а не только строки и числа.

Скачать библиотеку Json.NET можно из NuGet.

Использовать JSON мы будем следующим образом.

  1. Создадим объект корневой директории и монтируем диск.
  2. При обращении к файлу на чтение или запись вытаскиваем из JSON-хранилища адрес файла с данными запрошенного объекта виртуальной файловой систему и отдаем или пишем уже его данные. Это нужно для того, чтобы при копировании на наш диск, например фильма, мы сбрасывали в JSON только метаданные. Файл не будет разрастаться до неимоверных размеров.
  3. При необходимости можно добавить любые функции. Например, шифрование: добавляем в JSON поле для хеша ключа шифрования, его соль и сам зашифрованный ключ. А процедуры чтения и записи будут проверять и использовать эти поля. Можно даже сделать шифрование не для всех файлов. В общем, большой простор для фантазии!

Структура объекта файловой системы

Каждая запись об объекте файловой системы должна хранить:

  • имя объекта;
  • тип объекта (файл/папка);
  • путь к объекту;
  • дату и время создания;
  • дату и время последнего доступа;
  • дату и время последнего изменения;
  • размер;
  • права доступа;
  • атрибуты;
  • метаданные (автор, комментарий, подпись, версия и так далее).

Чтобы можно было расширять и переименовывать объекты нашей файловой системы, мы не станем указывать абсолютные пути к файлам, а будем хранить индекс родительского объекта. Такой подход используется во всех современных файловых системах, таких как NTFS и ext4, и, как видите, работает. При удалении мы просто пометим запись как удаленную, не удаляя ее саму. В общем, все как у больших. Приступим к написанию.

Реализация объекта файловой системы

Создадим еще один класс (Ctrl-Shift-A) и назовем его, например, XakepFSObject.

Теперь добавляйте следующие поля (все с модификатором public; также их нужно инициализировать пустыми значениями — "" для строк, для чисел, false для булевых и DateTime.Now для DateTime):

  • String Name
  • bool IsDirectory
  • int Parent
  • int ObjectID
  • DateTime CreatedTime
  • DateTime LastWriteTime
  • DateTime LastAccessTime
  • long Length
  • FileAttributes Attributes (нужен System.IO)
  • FileSystemSecurity AccessControl (нужен using System.Security.AccessControl)
  • String DataLocation
использование библиотеки dokan
У вас должно получиться как-то так

Теперь подумаем. Наша файловая система будет работать в один поток (пока что) и, значит, при каждом обращении парсить весь JSON будет слишком медленно. Поэтому мы пожертвуем компактностью JSON в пользу скорости работы. Каждый объект нашей файловой системы будет иметь метод, упаковывающий его в строку JSON, и еще один, который будет распаковывать его. Мудрить с названиями не будем: назовем эти методы PackJson и UnpackJson соответственно. Вот их код.

PackJson

UnpackJson

Внимательный читатель, конечно, заметил странные функции PackObject и UnpackObject. Я про них не упоминал, так как они содержат всего по одной строчке кода.

PackObject

UnpackObject

Для чего это нужно? Не знаю, как у вас, но у меня при использовании словаря типа Dictionary<String,Object> после десериализации операции приведения к нужному типу возвращали null для многих типов, в то время как хранение каждого объекта отдельно и десериализация при инициализации объекта проходит нормально. Из-за этой проблемы я и решился на такой ход. Также у меня возникла проблема с десериализацией объектов типа FileSystemSecurity, поэтому пока что чтение/запись прав доступа будут отдавать null. Впрочем, пока эта файловая система управляется нашей программой, плевать нам хотелось на любые права доступа. Наша же программа с легкостью переназначит их, когда это будет нужно.

От простых объектов, вроде тех, что мы сейчас накодили, толку мало. Поэтому ~~можете удалять этот класс~~ создавайте еще один класс, который будет хранить дерево файловой системы и реализует основные операции с файлами. Назовем мы его XakepFSTree. Готовую реализацию вы сможете найти на GitHub.

Исходный код проекта целиком на GitHub.

Как прикрутить все это к нашему основному классу XakepFSClass?

Банальный Hello World у нас уже был, на этот раз реализовывать придется все. Ну или почти все. Часть кода я позаимствовал из RegistryFS и слегка адаптировал, поэтому он может пересекаться с существующим.

Пример главной функции вы можете посмотреть в файле XakepFSClass.cs. Это — главная функция. Она отвечает за создание и открытие файлов, проверку существования файлов и много чего еще. Самое интересное тут не то, как мы создаем файл или проверяем его существование, а то, как мы обрабатываем переименованные файлы.

Сами файлы хранятся отдельно, а в JSON мы пакуем лишь метаданные. Вот только при переименовании файла в файловой системе записи в JSON обновятся, а на диске файлы-хранилища — нет. Поэтому, если мы создадим, например, файл example.txt, переименуем его и попробуем создать там еще один example.txt, мы получим ошибку, гласящую, что файл уже существует и создать новый с таким именем нельзя. Так не пойдет, поэтому я решил сделать очередной костыль: файлы-хранилища будут называться случайными именами без расширений.

Чтобы сделать действительно случайное имя, я беру относительный путь к файлу в виртуальной файловой системы, дописываю в конец текущую дату и время и беру от этой строки хеш MD5. Все, теперь создание множества файлов с одинаковыми именами не вызывает проблем! Естественно, этот же финт работает и для папок. Если нет жеалния изобретать велосипед, можете взять мою функцию хеширования, мне не жалко!

public NtStatus FindFilesWithPattern

В этом методе — шикарный пример того, как делать не стоит. По сути, это просто обертка вокруг «обычной» FindFiles, только эта оболочка проверяет, что было передано в маску для поиска. За время тестирования я обнаружил, что чаще всего там (в searchPattern) лежит звездочка либо имя файла. Вот только у меня, когда передавалось имя файла, префикс мог быть /, мог быть \, а мог и вообще отсутствовать. Не знаю, с чем это связано, но я перестраховался и сделал проверку на случай всех трех вариантов.

public NtStatus GetFileInformation

Из названия метода понятно, что мы отдаем метаданные о файле, подтягивая их из JSON. Самой работы с JSON вы здесь не увидите, она спрятана под капотом объекта класса XakepFSTree, который у меня называется FSTree и содержит среди прочего методы для создания и удаления файлов в одну строку. Прошу прощения, если это осталось непонятным при изучении кода двух прошлых методов.

Предпоследняя строка этого метода может вызвать справедливые вопросы у читателя. Поясняю: во время работы мы как правило не очищаем память за объектами, которые не используем. Поскольку функция GetFileInformation вызывается часто, имеет смысл поместить код очистки памяти (на самом деле просто вызов штатного сборщика мусора) здесь. Если вы хоть раз имели дело со средой CLR, то знаете, что на время работы сборщика мусора приостанавливается вообще вся программа, а не только поток, его вызвавший. Поэтому в этом методе мы вызываем сборщика с шансом 1/9.

public NtStatus ReadFile

Этот метод — святая святых нашей файловой системы, поскольку именно он отвечает за чтение данных. Тут все вроде бы просто: сначала создаем поток, затем читаем из него. При возникновении ошибки (обычно — файл занят другим потоком) повторить попытку. Но даже в таком простом методе я благополучно допустил ошибку, из-за которой вы не увидели эту статью еще неделю назад. Суть — лучше не изобретать велосипед и воспользоваться готовым решением, что я в итоге и сделал.

public NtStatus WriteFile

И на закуску — последний на сегодня и почти не уступающий по важности предыдущему метод. Он отвечает за запись в файлы. Тут все по аналогии с ReadFile, только доступ к файлу (последний аргумент конструктора FileStream) мы указываем Write.

Параметры монтирования Dokan

При помощи Dokan вы можете создавать новые диски, монтировать существующие и делать многое другое. Особого внимания заслуживает возможность создания дисков любого типа (да, не только локальные!) и разграничивать к ним доступ. Можно разрешить вашей файловой системе использовать несколько потоков. Все это передается в параметрах вызова Mount в классе Dokan. Рассмотрим эту процедуру подробнее.

Вообще, Dokan — штука потрясающе многофункциональная. Можно монтировать как файловую систему что угодно, даже системный реестр.

Первый параметр — это объект класса файловой системы, реализующий интерфейс IDokanOperations. Тут нет ничего запредельного, просто передаем new XakepFSClass(), и дело с концом.

Второй параметр — это точка монтирования. Причем, согласно документации, точкой монтирования может быть не только незанятая буква диска, но и любой пустой каталог NTFS. Правда, в таком случае не получится выбирать тип диска, но это не страшно.

Третьим параметром идет нужная комбинация элементов перечисления DokanOptions. Когда мы не указываем этот параметр, он по умолчанию становится равен нулю, что соответствует DokanOptions.FixedDrive. Это значение предписывает драйверу Dokan смонтировать обычный локальный диск. Другие значения позволяют выбрать другой тип диска. Самые интересные параметры — это NetworkDrive и RemovableDrive.

Естественно, из трех этих параметров в один момент времени можно использовать только один. С ними можно комбинировать CurrentSession, который заставит смонтировать этот диск только для текущего пользователя, MountManager, который сделает диск доступным для системного менеджера монтирования, и WriteProtection, чтобы заставить Dokan пометить диск как доступный только для чтения. По факту операции записи все равно могут передаваться в наше приложение, так что не забудьте дописать return NtStatus.Error; в метод WriteFile.

Четвертый аргумент укажет драйверу, сколько потоков можно одновременно использовать. По умолчанию там стоит единица, так что, если не хотите ничего менять, оставьте ее.

В пятом аргументе мы передаем версию. Какую? Я передаю туда Dokan.Version, но есть подозрение, что этот аргумент вообще ни на что не влияет.

В шестой параметр можно положить информацию о промежутке времени (объект TimeSpan), по прошествии которого файловая система будет автоматически отмонтирована, если пользователь не совершает с ней никаких действий.

Ну и наконец, седьмой параметр — путь к сетевой шаре в формате UNC ( \SERVER\Share). Этот аргумент имеет смысл выставлять, только если в третьем аргументе указано, что это NetworkDrive. В противном случае он будет проигнорирован.

Тестируем

Предлагаю не мудрить и просто скопировать на наш новоиспеченный диск несколько файлов, а затем посмотреть, как они открываются. Для чистоты эксперимента мы положим туда не только текстовые файлы, но и бинарные (пару фотографий и один PDF). В качестве текстового файла я возьму чистую HTML-версию одной из наших статей. Файл без нареканий скопировался на наш виртуальный диск и нормально открылся.

Теперь фотографии: я взял одну в JPG, а другую в PNG. Они также успешно записались на диск и нормально открылись.

Но все эти файлы не превышали мегабайта по объему. Как же наша файловая система поведет себя с большими файлами? Проверим, положив туда PDF-файл. Скопировался он очень быстро, я едва успел сделать скриншот, и открылся тоже нормально.

dokan

Как видите, все работает.

Выводы

Сегодня мы рассмотрели создание несложной (по возможности!) файловой системы. В отличие от FUSE, с Dokan нельзя написать всего пару строчек кода, чтобы заставить файловую систему работать. Зато логи ведутся подробнейшие прямо в консоли, что позволяет в случае чего без особых трудностей выловить ошибку и исправить ее. Лично мне это очень пригодилось, так что будьте готовы!

Также Dokan позволяет настроить любую мелочь, чего не сказать про FUSE, и, конечно же, стабильная и беспроблемная работа в Windows — несомненный плюс. Проект активно развивается, так что не забывайте своевременно обновляться. И еще один момент: если тоже будете делать что-то с Dokan (особенно на C++), не забудьте запостить в комментарии к статье ссылку на свой проект!

Понравилась статья? Поделиться с друзьями:
Добавить комментарий