Joplin это опенсорс приложение для управления заметками, которое я активно использую. Некоторое время назад понадобилось редактировать заметки автоматически. Задача казалась несложной, но по пути удалось собрать достаточно граблей. Пока разбирался, написал микро клиент на go. Заодно поднял в докере и интегрировал в приложение экспериментальный API консольного клиента. Делюсь найденной информацией и кодом.

Какая задача решается

Приложения Joplin используются на разных платформах и поддерживают несколько методов синхронизации данных. В моем случае синхронизируются через S3.

Есть несколько заметок с todo списками (например: работа, домашние дела, списки покупок, хобби). Каждый список в своей заметке. Нужно по определенному признаку выбирать, сортировать и выводить в специальную заметку актуальные задачи. То есть, некоторый автогенерируемый todo лист "на ближайшее время".

Задача в общем виде: периодическое получение данных заметок, обработка и обновление контента выбранной заметки.

Поиск решения

Очевидная идея использовать API. Документация приложения и гугл указали на некий web clipper API. Информации по нему маловато. В CLI приложении режим server с пометкой experimental feature. Первые попытки не увенчались успехом, временно отложил. Ок, что есть еще?

Существует Joplin Server: имплементация сервера синхронизации от авторов приложения. Его API не публичный, сервер не задумывался как API для сторонних клиентов. Технически применить его возможно. Но использовать не публичный API не хочется. Может быть есть еще варианты?

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

Решение 1: ходим напрямую в S3

Для написания клиента придется разобраться с форматами файлов

Файл заметки

Структура файла заметки простая: это текстовый файл .md. Содержит заголовок, тело, метаданные. Разделитель пустая строка:

Заголовок

Контент

id: f23f0132f2124bdea9db91c747853434
parent_id: 55f71434e23b412cb87d3b1cbcff823b
<...>

При редактировании важно не забыть обновить значение параметров updated_time, user_updated_time. Так остальные клиенты подхватят изменения файла при синхронизации.

Файл блокировки

Кроме редактирования заметки, важно не забывать, что есть и другие клиенты, выполняющие в это же время синхронизацию. За консистентность данных отвечает файл блокировки. Он представляет из себя файл с названием формата
locks/<lock_type>_<app_type>_<app_id>.json
Содержимое файла, судя по документации, не имеет значения: в теле те же данные что и в названии. Вероятно тело файла это легаси. На всякий случай будем писать тот же json объект, который пишется в файлы блокировок в других приложениях:

{"type":2,"clientType":1,"clientId":"f5fe97768f3f4188b062a55619b8753e"}

Нас интересует блокировка на запись, exclusive lock в терминах Joplin.

Механизм взятия блокировки из документации

  • Проверяем наличие блокировок

  • Если блокировок нет, создаем файл блокировки и возвращаемся к проверке

  • Если блокировка есть, проверяем владеем ли мы ей. Если мы не владеем блокировкой, возвращаемся к проверке

  • Если блокировка есть и мы им владеем, выполняем изменение

  • Снимаем лок

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

Задача обновления заметки

  • получаем лок на запись

  • процессим:

    • читаем .md файлы

    • фильтруем по parent_id

    • обрабатываем данные

    • собираем контент обновляемой заметки

    • если данные для обновления не изменились, выходим

    • обновляем заметку

  • снимаем лок

Результат можно найти в репозитории

Решение 2: web Clipper API

А если не изобретать велосипед клиент, и не трогать файлы напрямую? У приложения есть API, осталось научиться его готовить. И что вообще такое этот web clipper API?

Web Clipper API это HTTP интерфейс для браузерных расширений. В настройках десктоп приложения можно запустить веб сервер для интеграции с браузером. А кроме GUI у Joplin есть CLI версия. И в CLI приложении можно тоже запустить режим сервера. Итого: запускаем cli, настраиваем синхронизацию, включаем API. Редактируем заметки по API.

Пробуем завернуть в докер. Прокидываем конфигурацию в формате json и стартуем в режиме сервера: joplin server start. Приложение слушает 127.0.0.1:41184. При запуске дополнительно понадобится socat чтобы проксировать на 0.0.0.0. Параметр sync.interval по какой-то причине не запускает синхронизацию. Ок, просто периодически запускаем cli команду joplin sync. API запущено и доступно.

Теперь остается дописать в уже созданное приложение http клиент и реализацию joplin провайдера для http клиента.

Пример реализации и обернутый в докер API в репозитории.
Я реализовал тот же интерфейс, что и для s3. Это не очень оптимально, но в качестве proof of concept сойдет.

Плюсы и минусы решений

Плюсы работы с S3

  • высокая скорость доставки изменений: app -> S3 -> target_client

  • возможности расширения не ограничены

  • формат синхронизации достаточно простой

Минусы работы с S3

  • расширять функциональность сложно, придется разбираться с логикой и форматами

  • используется один конкретный протокол

Плюсы работы с API

  • нет зависимости от формата заметки

  • нет зависимости от протокола синхронизации

  • реализована основная функциональность

Минусы работы с API

  • скорость доставки изменений ниже: схема app -> CLI -> S3 -> target_client

  • сложнее инфраструктура: нужно поднимать дополнительный сервис с API

  • в 2 раза больше дискового пространства (S3+CLI клиент)

  • CLI server экспериментальная опция

  • возможности ограничены методами API

Заключение

Как часто бывает, выбор того или иного решения это компромисс. Я выбрал работать с S3 напрямую. Но также хорошо иметь возможность быстро реализовать нужную функциональность работая по API. Я не стал проверять работу API joplin server, но думаю такая интеграция тоже будет работать.

Спасибо за внимание!

Ссылки

https://joplinapp.org
https://github.com/laurent22/joplin
Как работает lock в joplin
Joplin web clipper

Исходный код

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


  1. gmtd
    08.01.2024 18:51
    +1

    Спасибо за статью, отличное начало

    1. Я правильно понял, что с файлами на S3, OneNote, DropBox можно работать напрямую? Потому как из документации Joplin работа идет через сервер

    Архитектура

    1. Как вы на S3 находили нужные файлы? Там же еще есть diff файлы.

    2. Если ответ на 1. - "да", то получается новый клиент для Joplin написать совсем нетрудно? Почему же их нет? Мне нравится Joplin, но UX у него ужасный.


    1. andrdru Автор
      08.01.2024 18:51
      +1

      Спасибо

      1. Я упомянул про инсталляцию joplin server как цели синхронизации, в self-hosted варианте. Это тот случай, который я не стал реализовывать: не захотелось лезть в закрытый API. Технически это даже проще, чем поднять API от CLI, надо только поизучать запросы. И все клиенты переключить на self-hosted сервер как цель синхронизации
        В моем случае цель синхронизации это бакет в S3, и клиент синхронизируется с ней

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

      3. Да, UX такой себе. Joplin offline-first софтина с возможностью синхронизации. Написание альтернативного клиента сведется к написанию своего софта для заметок, который будет совместим с форматом хранилищ. Ограничений нет, просто в реальном клиенте куча деталей вроде упомянутых diff файлов
        Для клиента я бы начал с поддержки API сервера как раз, хоть он и не публичный.


      1. gmtd
        08.01.2024 18:51

        То есть, вы по логам определили имя файла заметки и дальше уже работали с ним? А что-то более высокоуровневое можно из S3 пакета файлов получить? Определить все Joplin notebooks, определить файлы заметок, входящие в определённый notebook, построить всё дерево - такое возможно? Не нашел в документации ничего по этому поводу.

        Я бы не хотел привязываться ни к Joplin server, ни к CLI, если есть возможность работать с файлами напрямую.


        1. andrdru Автор
          08.01.2024 18:51
          +1

          Нет, такую информацию можно получить только выгрузив контент файлов. В названии в S3 используется формат <id>.md. Все, что касается типа файла, родительского элемента итп лежит в метаданных в самом файле. Например, параметр type_будет определять тип.
          То есть, в любом случае для S3 изначально придется получить все файлы, чтобы по ним построить зависимости. Далее можно при повторной синхронизации подтягивать изменения и применять их к локальным данным. Так делают сейчас клиенты, судя по их логам при синхронизации


          1. gmtd
            08.01.2024 18:51

            Да, я сейчас поэкспериментировал - надо всё сгрузить и построить дерево. И потом от времени последней синхронизации смотреть на S3 измененные файлы. Не очень логика, как мне кажется.

            Diff, в принципе, можно отключить в клиенте. В любом случае, он вроде идёт "назад", то есть, изменения от текущей ревизии в прошлое.

            Спасибо, очень полезная информация.


            1. andrdru Автор
              08.01.2024 18:51

              Да, логика не очень, но что имеем

              Пожалуйста, рад что ресерч оказался полезен:)


  1. maxzh83
    08.01.2024 18:51
    +1

    В свое время переехал с Joplin на Obsidian в том числе из-за того, что на нем проще синхронизировать заметки между разными устройствами.


    1. andrdru Автор
      08.01.2024 18:51

      Пробовал Obsidian, но уже не помню почему отказался в пользу Joplin. А чем синхронизация проще? Если верно понял, для Obsidian можно просто синхронизировать каталог с данными сторонним софтом?

      Синхронизация Joplin плохо работала для WebDAV с яндекс.диском, для больших файлов. Для S3 полет нормальный


      1. maxzh83
        08.01.2024 18:51
        +2

        Если верно понял, для Obsidian можно просто синхронизировать каталог с данными сторонним софтом?

        Да, просто каталог. Синхронизировать можно как угодно. Я использовал SyncThing. Настраивать не так, чтобы удобно, но делается это один раз. А так можно всякими Дисками, dropbox'ами и т.д. А если занести Obsidian денюжку, то все будет из коробки.


        1. LeshaRB
          08.01.2024 18:51

          У этого подхода есть минус
          Прежде чем начать редактировать надо сперва не забыть забрать изменения

          Случайный пробел, и все файл перетерся на сервер
          Я использовал GoodSync и FolderSync на телефоне


          1. maxzh83
            08.01.2024 18:51

            Прежде чем начать редактировать надо сперва не забыть забрать изменения

            SyncThing может запускаться в фоне (пробовал на windows и android) и автоматически отслеживать и подтягивать изменения