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

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

Однажды перед нами встала задача показать файлы с удаленного сервера нативно в Finder. Первое, что нам пришло на ум, — использовать WebDAV, но это был публичный сервис, к серверу которого у меня не было доступа, а в распоряжении имелся только REST API. В голове промелькнула мысль написать свою файловую систему, но она казалась слишком амбициозной. Однако мои сомнения развеялись, когда я обнаружил, что сообщество энтузиастов развивает проект FUSE for macOS, который сводит создание собственной файловой системы к нескольким сотням строк кода. А идущий в комплекте фреймворк не требует унылых разбирательств с API на голом C и вполне пригоден для использования в комплекте с современным и мощным Swift.

Что такое FUSE?

FUSE (Filesystem in Userspace) — это интерфейс для программ пространства пользователя, позволяющий экспортировать файловую систему ядру ОС. Этот механизм появился в Linux, и условно его можно разделить на два базовых компонента: модуль ядра (поддерживается разработчиками ядра) и библиотека пользовательского пространства (libfuse). Эта библиотека предоставляет методы для монтирования, размонтирования, отправки запросов к ядру и получения ответов от него. Также она реализует удобный верхнеуровневый API, в котором мы можем оперировать привычными понятиями имен файлов и путей, вместо работы с inode.

Проект FUSE for macOS представляет собой аналогичный набор API (а также Objective-C фреймворк), позволяющий реализовать полноценную файловую систему, которая будет работать в пространстве пользователя на macOS. Так как его API является надмножеством FUSE API из Linux, то существует теоретическая возможность завести многие из существующих файловых систем на macOS. В настоящее время этот проект остается единственной реализацией FUSE для macOS, которая развивается и поддерживается силами сообщества, хотя и активность на GitHub и в Google Groups сейчас довольно низкая.

Установка фреймворка

Установка не отличается сложностью: скачиваете инсталлятор с сайта разработчика и запускаете его. Если предпочитаете собирать такие вещи из исходников, то это тоже не составит труда: достаточно установить зависимости через brew и запустить сборочный скрипт, все это подробно описано в Readme на GitHub.

Настройка проекта

Создание файловой системы на FUSE и Swift
Создание нового проекта в Xcode

Создадим новый проект в Xcode. Это должно быть Cocoa Application (в разделе macOS), я назвал его HelloFuse, язык выберем Swift, остальные параметры можно выбрать на свое усмотрение.

Подключим фреймворк

Создание файловой системы на FUSE и Swift
Расположение фреймворка OSXFUSE

После установки фреймворк будет расположен по следующему пути: /Library/Frameworks/OSXFUSE.framework. Чтобы добавить его в проект, достаточно просто перетащить его в раздел Linked Frameworks and Libraries на вкладке General настроек сборки.

Создание файловой системы на FUSE и Swift
Подключение фреймворка

Создадим Bridging Header

Так как мы пишем проект на Swift, а фреймворк реализован на Objective-C, то нам нужно создать и подключить так называемый Bridging Header. Создадим заголовочный файл (File > New > File > macOS > Source > Header File), назовем его HelloFuse-Bridging-Header.h и добавим в него следующую строчку:

Теперь на панели навигации выбираем наш проект, выбираем сборку в разделе Targets, переходим на вкладку Build Settings, находим раздел Swift Compiler → General, в поле Objective-C Bridging Header добавляем

Создание файловой системы на FUSE и Swift
Подключаем Bridging Header в настройках проекта

Отключим Sandbox

По умолчанию во всех приложениях включена песочница, которая ограничивает возможности приложения, но в отличие от iOS на macOS ее можно отключить. Этим ты потеряешь право распространять приложение через App Store (что тоже не будет проблемой в случае с macOS), но в нашем случае нам нужен полноценный доступ к файловой системе, поэтому выбора нет.

Перейдем на вкладку Capabilities в настройках сборки и поставим переключатель в пункте App Sandbox в положение OFF.

Hello world

Описание файловой системы

Поведение файловой системы описывается в отдельном классе. Создадим класс под названием HelloFS и унаследуем его от NSObject. В минимальном примере нам понадобится реализовать только два метода: получение списка файлов, который мы будем отображать, и содержимое каждого файла.

В методе, отвечающем за отображение файлов, нужно вернуть массив строк с именами файла. В качестве параметра туда приходит путь (path), в более сложных случаях нужно будет его обрабатывать, чтобы показывать контент соответствующей директории. Здесь я просто возвращаю один файл hello.txt.

В метод, который отвечает за отображение пути файла, аналогично приходит путь, в зависимости от которого мы должны решить, какое содержимое отдавать для файла. В нашем же примере мы будем для всех файлов возвращать строку «Hello world!».

В итоге файл HelloFS.swift примет следующий вид:

Инициализация файловой системы

В классе AppDelegate объявим две переменные:

В метод applicationDidFinishLaunching добавим следующий код:

Код достаточно интуитивен: инициализируем файловую систему, указываем название раздела и передаем параметр, что она только для чтения, а затем монтируем ее по указанному пути.

По завершении работы приложения в методе applicationWillTerminate демонтируем нашу файловую систему:

После запуска приложения наш раздел появится в директории /Volumes, a также должен быть виден и в корневой директории. В разделе будет лежать единственный файл hello.txt, в котором будет написано «Hello world!».

Создание своей файловой системы на FUSE Swift
Результат работы нашего Hello world проекта

Установка собственной иконки для раздела

Для раздела можно заменить иконку по своему усмотрению, для этого в проект нужно положить иконку в формате *.icns (требования к размерам и прочему можно найти в macOS Design Guidelines) и добавить путь к ней в массив options.

Файловая система для фото из VK

Давай попрактикуемся и реализуем то, для чего в большинстве случаев создаются подобные файловые системы, — отображение контента с удаленного сервера в виде файлов и папок. Предлагаю отобразить таким образом альбомы и фотографии из паблика нашего журнала в VK. Пример можно будет легко адаптировать для своих нужд, так как мы не будем завязываться на SDK «ВКонтакте», а будем обращаться напрямую к методам REST API, доступным без авторизации.

Модели данных

Мы будем оперировать двумя сущностями: альбом и фотография. Альбомы будут лежать в корне нашей файловой системы и выглядеть как папки, а фотографии находиться в альбомах и выглядеть, соответственно, как файлы картинок. Наши модели должны удовлетворять протоколу Decodable, для того чтобы мы могли их распарсить из JSON, который мы получим с сервера.

Для альбома нам понадобится знать его идентификатор, чтобы потом по нему запросить фотографии, а также его название.

Для фотографий нам нужно знать URL, по которому мы будем скачивать фотографию, а также имя файла. Так как в VK нет отдельного заголовка для фотографий, я буду использовать имя файла из URL.

Ответ сервера VK имеет следующую структуру:

Интересующая нас информация лежит в items. Чтобы с такой структурой было удобнее работать, сделаем вспомогательную модель VKResponse.

Сервис для получения данных

Очевидно, что получать данные из сети не входит в обязанности класса, отвечающего за построение файловой системы, поэтому мы вынесем эту функциональность в отдельный класс, который мы назовем VKService (в боевом проекте его тоже следовало бы разбить на несколько слоев, но здесь мы не будем усложнять). Коротко опишу основные моменты: методы fetchPhotos и fetchAlbums делают GET-запрос при помощи URLSession к API vk.com и парсят полученный JSON при помощи JSONDecoder. Остальные методы предназначены просто для удобного получения соответствующих сущностей (фото из альбома, альбом по пути в файловой системе и прочее). Так как он имеет мало отношения к нашей сегодняшней теме, я не буду останавливаться на нем подробно, ты сможешь его найти в исходниках проекта.

Описание файловой системы

Алгоритм отображения контента будет следующий. Проверяем, находимся ли мы в корневой директории, если да, то возвращаем список альбомов; иначе предполагаем, что мы вошли в альбом, пытаемся получить его и отобразить список фотографий.

Методы requestAlbums() и requestPhotos() нам нужны, чтобы запросить у нашего сервиса соответствующий контент.

Обрати внимание на вызов метода NSWorkspace.shared.noteFileSystemChanged. Существует проблема с тем, что SDK FUSE for macOS ожидает данные синхронно, соответственно, нам нужно будет как-то обновить список файлов, после того как он вернется с сервера. Для этого мы и вызываем упомянутый метод: он сообщит файловой системе, что нужно обновить контент по переданному пути, и метод contentsOfDirectory будет вызван еще раз.

Для простоты здесь данные запрашиваются только один раз, но в боевом решении, конечно, должен быть некий кеширующий сервис, который будет возвращать актуальные данные, при необходимости обновлять их.

Альтернативой (а иногда единственным выходом) может быть решение обращаться к серверу синхронно. Так я делаю в методе получения самой фотографии.

Метод getPhotoData, принадлежащий классу VKService, получает данные синхронно, используя метод sendSynchronousRequest класса NSURLConnection. Синхронное получение данных будет выглядеть следующим образом:

Этот метод в настоящее время помечен Apple как устаревший, поэтому альтернативно можно использовать DispatchSemaphore в комплекте с URLSessionDataTask.

У вдумчивого читателя к этому моменту должен был возникнуть вопрос: а как файловая система определяет, показать файл или папку? Для этого необходимо переопределить еще один метод, attributesOfItem.

Здесь мы определяем, какие атрибуты выставить для файла по заданному пути. Я проверяю, если мы можем получить альбом для этого пути, то выставляем тип «Директория», иначе — «Файл». Для корневой директории нам не требуется возвращать никаких атрибутов.

Создание своей файловой системы на FUSE и Swift
Отображаем альбомы и фотографии из VK в нашей файловой системе

Что дальше?

В сегодняшнем материале мы рассматривали примеры исключительно read-only-систем, но нужно понимать, что реализовать запись и удаление файлов тоже не сложно: достаточно аналогичным способом переопределить соответствующие методы, с полным перечнем которых ты можешь ознакомиться в заголовочном файле OSXFUSE/OSXFUSE.h. Еще хотелось бы обратить внимание, что метод contents(atPath:) не единственный способ вернуть содержимое файла, для более сложных случаев можно реализовать полный цикл open/read/release, и авторы библиотеки рекомендуют именно этот способ, как более производительный.

Также за кадром осталось то, что FUSE for macOS умеет генерировать события для Notification Center, на которые можно подписаться (например, открывать Finder после того, как система примонтировалась). Пример использования ты также найдешь в исходниках.

Выводы и предостережения

C одной стороны, FUSE for macOS — самый простой способ реализовать собственную файловую систему для macOS, который предоставляет верхнеуровневый и довольно удобный интерфейс для описания ее поведения. На другой чаше весов мы имеем следующие недостатки: очень слабая документация — практически единственным источником знаний по этому проекту служат примеры авторов на GitHub; низкий уровень активности в проекте — хотя авторы в рассылке утверждали, что не забросили проект, дождаться ответов на свои вопросы в Issues на GitHub почти нереально. Еще хотелось бы отметить плохо проработанную систему ошибок: если ты сделал что-то неправильно, скорее всего, тебе не прилетит ошибка, а просто перестанут отображаться файлы без каких-либо логов в консоли и прочего. Резюмируя вышесказанное: прежде чем использовать этот проект в продакшене, ты должен быть морально готов самостоятельно разбираться с большинством проблем и закладывать в оценку проекта возможные риски.

Михаил Овчинников

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