Давным, давно ... искал средство организации видео-файлов.

Чтобы можно было каждой киношке назначить какие-то маркеры, тэги. Выбирать файлы по наличию тэгов. Или наоборот - по отсутствию тэгов. Например: детектив, фантастика, не-ужасы.

Чтобы всё работало в обычном проигрывателе: кликнул "Открыть файл..."; выбрал Детектив, Фантастика, не-Ужасы; получил список фильмов и выбрал нужный. Без специальных файловых менеджеров и браузеров.

И тогда такое средство мне не нашлось. А сейчас ... сейчас решил сделать его сам.

Что в результате получилось можно увидеть по ссылкам: демо-видео, код.

Далее будет разбор, как это делалось, какие выяснились тонкости и возникли вопросы. Так как это всё таки туториал, то будет и описание базовых вещей и помимо прямых путей будут вспомогательные описания - возможно, они будут полезными для понимания.

Общие вопросы реализации

  • Технология

Если поискать подходящую технологию, позволяющую организовывать файлы «по‑своему», то очень быстро натыкаешься на три буквы vfs: виртуальная файловая система. По описаниям и возможностям эта технология очень хорошо ложится на задачу. Кроме того, она «локальная» — для работы не нужно каких‑то внешних серверов, облаков и др.. И «легковесная» — для реализации требуется сделать модуль ядра, данные можно хранить в файле с простой структурой, никаких СУБД и т. п.. К минусам такого подхода можно причислить то, что это будет именно модуль ядра: потребуется сборка на каждом компьютере своей версии с установкой компилятора, заголовочных файлов и т. п.

Ок, попробуем сделать свою виртуальную файловую систему. И назовём её tagvfs.

  • Представление файлов

Прежде всего необходимо было определиться со способом хранения и представления видео-файлов в tagvfs (это создаваемая виртуальная файловая система). Здесь есть несколько соображений:

Во-первых, категорически не хотелось сохранять копии файлов в самой tagvfs: это занимает много-много места, создание копии потребует существенного времени (видео-файлы обычно тяжёлые). Также такой способ хранения ограничивает работу с файлами в "обычном" режиме: ведь храниться могут не только видео-файлы, но ещё какие-либо документы. Документы можно редактировать; хранить совместно с другими документами (например, исходный документ и его экспорт в pdf) и т.п. Поэтому желательно, чтобы файлы имели своё "постоянное" место хранения в обычной файловой системе, а tagvfs отображала бы эти файлы.

Во-вторых, вполне допустимо в своей виртуальной файловой системе вычитывать внешние файлы и выдавать их как свои. У такого подхода даже можно найти некоторые преимущества. Например, возможность реализовать операции из категории "ненормальное программирование": доступ к файлам в обход прав, модификация данных при чтении/записи, блокирование определённых областей и др.. Минусы у подхода тоже значительные: прежде всего это разборки с правами на файлы (кто что может читать, записывать и др.). И, конечно же, все эти операции на файлах нужно реализовать в коде.

В-третьих, остаётся вариант с символьными ссылками на файлы. Для управления видео-файлами это очень хороший вариант. Из минусов подхода - некоторое неудобство при добавлении файлов в tagvfs: приходится вместо копирования делать символьные ссылки. В остальном - одни плюсы: нет нужды самому опрашивать файлы и хранить информацию о них (как минимум наличие файлов и их размер). И обработкой прав на файлы также занимается линукс.

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

Модуль ядра (для версии 5.10)

Модули ядра - это специально оформленный код, который ядро операционной системы загружает и запускает.

  • Инициализация

С программной точки зрения модуль ядра должен содержать функцию инициализации и может иметь функцию завершения.

Задавать функции можно двумя способами:

  1. Через макросы:

static int tagvfs_init(void) { ... ; return res; }

static void tagvfs_exit(void) { ... }

module_init(tagvfs_init);
module_exit(tagvfs_exit);
  1. Явное объявление функций:

int init_module(void) { ... ; return res; }
void cleanup_module(void) { ... }

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

Функции инициализации должна быть одна на модуль. Конечно, если её вообще не указать, то модуль скомпилируется, но что с таким модулем делать - непонятно. Функции завершения также должна быть одна на модуль; либо её может не быть, если операции по очистке ресурсов не требуются. Более того, если модуль вкомпилирован в ядро, то функция завершения вообще не вызовется, даже если она есть.

  • Возвращаемые значения

Функция инициализации возвращает результат своей работы через int. В данном случае всё довольно очевидно: значение 0 означает успешную инициализацию, отрицательное значение указывает на ошибку. Так исторически сложилось, что во многих функциях возвращаемое значение кроме результата (который стараются делать неотрицательным) может содержать ещё информацию об ошибке. Всё немного усложняется, если функция возвращает адрес. Адрес тоже можно привести к знаковому числу и проверить на отрицательность, однако даже валидный адрес после преобразования может выдать отрицательное значение. Поэтому за ошибку принимается не любое отрицательное число (и в случает адреса и в случае значения int), а только от -4095 до -1. (ВНИМАНИЕ: Это для ядра 5.10).

Конечно же, для работы с такими значениями есть макросы:

IS_ERR_VALUE, IS_ERR - для проверки целых чисел и адресов;

ERR_PTR, PTR_ERR - для преобразования кода ошибки в адрес и наоборот.

Подробнее про макросы можно посмотреть здесь: https://elixir.bootlin.com/linux/v5.10/source/include/linux/err.h .

Для случая, когда при успехе возвращается 0 (и таких функций много), используется, наверное, самая простая проверка на ошибки:

if (res) { ... обработка ошибок ... }

Что касается самого возвращаемого значения в случае ошибок, то рекомендуется пользоваться штатными константами ядра, соответствующими случившейся ошибке, и не выдумывать своих значений. Связано это с тем, что линукс помимо кода ошибки будет ещё выводить текстовую расшифровку и если, например, программист на любую ошибку будет возвращать -1, то пользователь будет искать более мощную утилиту sudo, т.к. увидит что-то наподобие:

insmod: ERROR: could not insert module tagvfs.ko: Operation not permitted

Часто используемые константы (со знаком минус): -ENOMEM, -EINVAL. И список можно посмотреть здесь: https://elixir.bootlin.com/linux/v5.10/source/include/uapi/asm-generic/errno-base.h .

  • Информация о модуле

В коде модуля можно сделать ряд деклараций, которые говорят о лицензии модуля, его авторе, содержат описание и т.д.. Конечно, их можно не указывать. Однако, на этапе компиляции вам обязательно напомнят про отсутствующую лицензию. А ещё в ядре есть функция license_is_gpl_compatible, которая по названию лицензии определяет, насколько она GPL. В общем, лицензию лучше указать, да автор с описанием не будет лишним. Всё это можно задать например так:

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Author");
MODULE_DESCRIPTION("Description information");

Особенность этих макросов в том, что их можно указывать по несколько для одного модуля: даже лицензий можно указать несколько (и какая будет использоваться в ядре?). Более того, это рекомендуемый способ для указания нескольких авторов: каждый указывается в отдельном макросе.

Указанную информацию можно потом прочитать через утилиту modinfo и получить что-то наподобие:

description:    VFS module to organize files by tags
author:         Evgeny Kislov <dev@evgenykislov.com>
license:        GPL
name:           tagvfs
vermagic:       5.10.0-23-amd64 SMP mod_unload modversions 

Помимо заданных в коде данных есть ещё одна очень важная информационная строка: vermagic. В ней содержится версия ядра, под которую собран модуль.


Продолжение - оно будет.


Заставка является изображением, сгенерированным нейросетью.

Комментарии (18)


  1. 9982th
    04.08.2023 00:35
    +1

    А вы точно хорошо искали? Прямо сейчас я вижу в поиске не меньше полудюжины названий, например, здесь. Самые старые из нулевых, самые свежие — годичной давности. Неужели они все не подходят?


    Та, которой мне приходилось пользоваться, распространялась в виде одного .jar файла без всяких модулей ядра и работала от простого пользователя.


    1. pred8or
      04.08.2023 00:35

      Выглядит так, что из того что там упомянуто, только tmsu подаёт признаки жизни


    1. Apoheliy Автор
      04.08.2023 00:35

      Видимо, всё-таки ДОСТАТОЧНО хорошо искал. Посмотрел пару решений, и обнаружились тонкости. Далее на примере TMSU. Получить список файлов можно двумя способами:

      • по одному тэгу: т.е. всего один критерий. Очевидно, что этот способ не подходит;

      • заранее сделать запрос на объединение тэгов и получить список фалов. Ключевой момент - ЗАРАНЕЕ. Пример с сайта:

      $ ls mp/queries
      README.md
      $ mkdir "mp/queries/mp3 and not folk"
      $ ls "mp/queries/mp3 and not folk"
      summer.1.mp3
      winter.3.mp3

      т.е. у вас не будет возможности при открытии файла в проигрывателе выбрать комбинацию тэгов "здесь-и-сейчас". И это плохо-плохо.

      От себя добавлю, что создание запроса "заранее" - оно очень сильно облегчает реализацию всей файловой системы. Ключевой момент здесь в том, что количество этих запросов известно и оно небольшое, явно меньше всех комбинаций тэгов.


      1. iliazeus
        04.08.2023 00:35

        у вас не будет возможности при открытии файла в проигрывателе выбрать комбинацию тэгов "здесь-и-сейчас"

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


        1. Apoheliy Автор
          04.08.2023 00:35

          Возможность выбора папок без клавиатуры, только кликами мышки может быть очень полезной, когда сидишь в VR-шлеме.

          Сарказм:

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


      1. 9982th
        04.08.2023 00:35
        +1

        Я поискал за вас.


        TSMU:


        $ ls mp/tags
        big-jazz mp3 music
        $ ls -l mp/tags/music
        drwxr-xr-x 0 paul paul 0 2012-04-13 20:00 big-jazz
        drwxr-xr-x 0 paul paul 0 2012-04-13 20:00 mp3

        Судя по всему произвольные комбинации тегов вполне поддерживаются.


        Tagsistant: прямо написано, что поддерживается:


        You can even combine the results of more than one query by placing a +/ in between, like in startrek/+/starwars/ which merges the files tagged startrek with the files tagged starwars.

        jtagsfs:


        You cd to /path/to/mountpoint/tags/video/HD/thriller/sci-fi/@ (the order isn't important) and copy your file there.

        tagfs:


        /tag0/tag1 (starting at the tagfs root) retrieves all files that are marked with both tag0 and tag1. Prepending a '-' negates a tag, selecting every file that is not marked with the tag.


  1. saege5b
    04.08.2023 00:35

    А чем хардлинки с много-много каталогов не устроили?


    1. iliazeus
      04.08.2023 00:35

      Расскажите подробнее, что вы имеете в виду.

      Если на каждый тег делать по каталогу, и внутри них каталоги с остальными тегами и так далее, то получится экспоненциальная вложенность. Не уверен, насколько это будет удобно и быстро работать.

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


      1. saege5b
        04.08.2023 00:35

        У меня 2-4 уровня вложенности получается.

        Фильмы:

        Документальное

        ....

        .....

        Образовательное

        Физика

        Биология

        ....

        .....

        и т.д.

        С ситуацией тотального удаления не сталкивался, это обычно бывает когда появляется версия с лучшим качеством.


        1. iliazeus
          04.08.2023 00:35

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


          1. saege5b
            04.08.2023 00:35

            В пределах диска, куда скопируешь хардлинк, там файл и будет. У некоторых файлов с десяток имён.


            1. iliazeus
              04.08.2023 00:35

              Думаю, вы меня не совсем поняли. Я хочу найти, к примеру, все фильмы, которые относятся одновременно к категориям "комедия" и "научная фантастика". Как их удобно найти в вашей структуре?

              У систем тегов, как правило, есть встроенные методы такого поиска, которые работают достаточно быстро.


    1. Apoheliy Автор
      04.08.2023 00:35

      • Хардлинки работают в пределах одного диска/файловой системы;

      • Все операции с каталогами (например, создать много-много вложенных каталогов с дубликатами) - это работа внешнего скрипта. Немного не удобно;

      • Операции с файлам (раскладывание по каталогам и удаление) - тоже скрипт. Файловыми операциями копирования/удаления обойтись не удастся;

      • Заполнение файлами "отрицательных" каталогов (не-Физика) - по-моему это будет Боль.


  1. iliazeus
    04.08.2023 00:35
    +2

    Почему вы решили реализовать именно модуль ядра, вместо использования FUSE?


    1. Apoheliy Автор
      04.08.2023 00:35

      Это решение было скорее перестраховкой на всякий случай, так как выяснились моменты, например:

      • вложенность тэгов сразу приводит к динамической генерации путей. И количество всевозможных вариантов таких путей явно превышает возможности компьютера. Например, количество комбинации для 21 тэга (даже без учёта "не-" тэгов) уже больше максимального 64-битного числа. Т.е. никакие фиксированные inode вас не спасут;

      • операции с добавлением и удалением файлов в тэгах меняют содержимое в других тэгах. Например, если фильм удалить из папки детектив/фантастика/ужасы, то он сразу же удалится в папках детектив/ужасы; детектив/ужасы/фантастика и появится в детектив/фантастика/не-ужасы; детектив/не-ужасы ну и так далее. Очевидно, что сам линукс содержимое папок где-то у себя кэширует. Вот только непонятно было, к каким возможностям доступ дадут, к каким ограничат.

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


  1. KoCMoHaBT61
    04.08.2023 00:35

    А xattr не подходит?


    1. Apoheliy Автор
      04.08.2023 00:35

      Можно по-подробнее, а чем он может подойти? Видеопроигрыватель сможет отфильтровать по нему файлы? Или это способ хранения информации?


  1. TIEugene
    04.08.2023 00:35
    +1

    Вы переизобрели semantic web.
    Кстати, в KDE 3 планировалось нечто подобное, а даже работало в beta. Не взлетело.
    В Windows тоже пытались (кажется WinFS должно было называться). Аналогично.
    Но реализация неплохая, да.
    Хотя модуль ядра действительно перебор. FUSE было бы достаточно.