Одна папка вместо разбросанных файлов: как я сделал переносимое хранилище для офлайн-приложения на Flutter
Прошлый раз я писал про то, как заложил в схему БД поля под синхронизацию, которой пока нет. Синхронизации до сих пор нет, зато вышли два обновления: 0.2.0 и 0.2.1. В этой статье про три вещи, на которых пришлось подумать: переносимую папку с данными, бэкапы и импорт из Notion и Obsidian. Nookly — локальное приложение для заметок на Flutter: страницы, блоки, таблицы, канбан, поиск. Данные лежат в SQLite на устройстве, без облака.
Проблема: данные лежат где-то в системной директории
До 0.2.1 база, картинки и настройки жили там, куда их кладёт ОС для приложения: свой путь на Windows, свой на Linux. Для пользователя это означает, что вопрос «как перенести мои заметки на другой компьютер» не имеет простого ответа. Хочется, чтобы ответ звучал так: скопируй папку. Сейчас всё лежит в одной папке Nookly/: база, картинки, настройки, а рядом .backups/. Путь виден в настройках, папку можно открыть в проводнике или перенести. Если указать место, где уже есть база, приложение предложит открыть её, а не создавать новую поверх.
Почему просто скопировать data.db — не бэкап
Первая мысль для бэкапа: скопировать файл базы. С SQLite это работает хуже, чем кажется.
Если база в режиме WAL, свежие транзакции могут лежать не в основном файле, а в отдельном
-wal. Копия одного.dbпотеряет последние записи.Если копировать файл в момент записи, можно получить несогласованный снимок.
Для консистентной копии у SQLite есть штатные способы: backup API или команда VACUUM INTO:
VACUUM INTO '/path/to/backup.db';
Она создаёт самостоятельный, уже «сжатый» файл базы с согласованным состоянием, не требуя останавливать приложение.

backups. Раз в день при запуске в .backups/ пишется архив базы и настроек. Логика простая: при старте смотрим дату самого свежего архива, если прошли сутки, пишем новый и удаляем всё, что старше седьмого. Проверка «раз в сутки», а не «при каждом запуске», нужна затем, чтобы при частых перезапусках семь копий не превратились в семь версий одного и того же дня.

А в браузере папки нет
Веб-сборка живёт в браузерном хранилище, и кроссбраузерного доступа к произвольной папке на диске там нет. Поэтому вместо папки в браузере работают экспорт и импорт всего архива целиком. Сценарий «перенести на другой компьютер» остаётся тем же, меняется только способ.
Импорт из Notion и Obsidian
Главная причина не переезжать на новый инструмент — начинать с нуля. Поэтому в 0.2.0 появился импорт zip и Markdown: страницы, картинки и CSV-базы из Notion.
Экспорт Notion устроен так, что в имена файлов и папок добавляется длинный идентификатор, а ссылки между страницами — это относительные пути с URL-кодированием. Значит, чтобы перенести связи между страницами, недостаточно просто прочитать Markdown: нужно сопоставить пути из ссылок с импортированными страницами. У Obsidian всё проще: обычные .md и [[вики-ссылки]].
Форматирование внутри блока
В 0.2.1 в блоках появились жирный, курсив, подчёркивание, зачёркивание, код, цвет текста и фона, а также ссылки [[страница]], @, @дата и внешние.

Здесь есть неочевидная связь с поиском из прошлой статьи. Если разметка хранится прямо в тексте блока, индекс FTS5 будет цеплять служебные символы и адреса ссылок, поэтому индексировать нужно чистый текст без разметки.
Приложение: nookly.ru, репозиторий с релизами: GitHub, зеркало: GitFlic.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.