GUI (composeApp) — Compose Multiplatform for Desktop. Отвечает только за
отображение состояния и пользовательский ввод. Никакой бизнес-логики монтирования
здесь нет — только вызовы модуля rclone-bridge.
rclone-bridge — Kotlin-модуль, инкапсулирующий всё общение с rclone:
RcloneProcess — запуск и остановка rclone rcd как дочернего процесса.
Запуск асинхронный, поэтому есть awaitReady(): он опрашивает порт, пока rcd
не начнёт принимать соединения, и падает с внятной ошибкой, если процесс умер.
Вывод rcd перехватывается в кольцевой буфер (последние 200 строк) — именно его
показываем пользователю, когда что-то пошло не так. stop() сначала просит
процесс завершиться и только потом добивает: rclone должен успеть размонтировать
то, что смонтировал.RcloneTransport — как вызов доставляется до rclone. У RC API один и тот же
вид везде («метод плюс объект на входе, объект на выходе»), а доставка
разная: на десктопе рядом живёт процесс rclone rcd и вызов уходит к нему
по HTTP (HttpRcloneTransport), на Android отдельный процесс поднять нельзя
и rclone линкуется в приложение (см. раздел про мобильные платформы).
Разбор ответов от этого различия не зависит и остаётся общим.
Тело запроса транспорт сериализует сам, а не отдаёт объектом на откуп
ktor-плагину ContentNegotiation: клиент в RcloneClient можно передать
любой, и без установленного плагина отправка падает с невнятным
“Fail to prepare request body”. Тесты по этой же причине работают через
голый HttpClient без плагинов — иначе они проверяют не то, чем пользуется
приложение.
RcloneClient — типобезопасная обёртка над RC API поверх транспорта:
config/create, config/delete, config/listremotes — управление облаками;mount/mount, mount/unmount, mount/listmounts — монтирование;core/stats, job/status, job/stop — прогресс и состояние операций;core/version — заодно служит пробой живости rcd.Тело запроса собирается как JsonObject: у Map<String, Any?> с разнородными
значениями нет сериализатора, и такой вызов падал бы в рантайме.
{"error": ..., "status": 500}) разворачиваются в
RcloneRcException с текстом причины — чтобы до GUI доходило “job not found”,
а не голое “500”.GUI никогда не вызывает rclone напрямую через shell — только через этот модуль.
rclone (встроенный бинарник) — не форкается и не патчится, используется “как есть”, но ставить его отдельно пользователю не нужно: он едет внутри дистрибутива OpenDisk.
Как это устроено:
:composeApp:downloadRclone скачивает официальный архив с downloads.rclone.org
на этапе сборки;composeApp/build.gradle.kts: если сумма не сошлась, сборка падает, а не собирает
пакет с подменённым бинарником;appResources, откуда Compose Desktop переносит его внутрь
установщика и отдаёт приложению путь через compose.application.resources.dir;licenses/rclone-LICENSE.txt).В рантайме RcloneProcess.locate() ищет бинарник по приоритету:
opendisk.rclone.path или переменная OPENDISK_RCLONE;PATH — для тех, у кого rclone уже стоит.GUI показывает, какой именно бинарник используется, — чтобы источник никогда не был для пользователя сюрпризом.
Само монтирование делает rclone, но ему нужен драйвер файловой системы: WinFsp на
Windows, FUSE на Linux, macFUSE на macOS. WinFsp едет внутри дистрибутива (официальный
подписанный установщик, ~2 МБ, скачивается на этапе сборки с проверкой SHA-256) —
пользователь ставит его одной кнопкой из приложения, а не ищет по сайтам. Установка
всё равно требует прав администратора, поэтому запускается через ShellExecute с
глаголом runas: без него UAC просто не появится и установка молча провалится.
Успех определяется не кодом возврата msiexec, а повторной проверкой системы — пользователь может отменить UAC, и осмысленной ошибки в этом случае нет.
WinFsp распространяется под GPLv3 (со специальным исключением для FSD), поэтому рядом с установщиком в дистрибутив кладётся его лицензия, а сам установщик берётся неизменённым с официального релиза.
Доступность монтирования нельзя определять по mount/types. На Windows этот
эндпоинт отвечает ["cmount"] даже когда WinFsp не установлен, а падает уже сама
попытка монтирования — с сообщением «cannot find winfsp». Поэтому смотрим в систему
напрямую: реестр HKLM\SOFTWARE\WOW6432Node\WinFsp и каталог установки.
Статус «подключено» нельзя определять по полю Fs из mount/listmounts. Его
формат зависит от бэкенда: смонтировав disk:, для local получим
disk://?/E:/путь, а для alias — вообще разрешённый путь без имени облака.
Поэтому приложение запоминает точки монтирования, которые само же и создало, и
сверяется по MountPoint. Это корректно, потому что rcd — дочерний процесс
приложения и умирает вместе с ним: все живые маунты созданы в текущей сессии.
Бинарник rclone едет внутри дистрибутива. На Linux он приезжает в пакет с правами 644, то есть неисполняемым: права теряются внутри jpackage, и повлиять на это из сборки не удалось — ни настройкой прав при копировании ресурсов, ни chmod после него.
Приложение обходится само. RcloneProcess.bundled() сначала пробует снять
бит выполнения на месте, но в /opt, принадлежащем root, обычному
пользователю это не удастся. Тогда бинарник копируется в ~/.cache/opendisk,
где права наши, и запускается оттуда. Копия делается один раз и переиспользуется:
признаком служит совпадение размера с файлом в каталоге установки.
Это стоит ~85 МБ в домашнем каталоге на Linux и не нужно на Windows, где права файлов роли не играют. Если jpackage однажды начнёт сохранять права, обходной путь можно будет убрать — в сборке есть шаг, который показывает права на rclone в готовом пакете.
Используем родной rclone.conf, включая встроенное шифрование паролем. OpenDisk не
хранит пароли и токены облаков в собственном формате — это исключает дублирование логики
шифрования и известные грабли самопального хранения секретов. Настройки остаются общими
с консольным rclone: пользователь может править их привычным способом.
RcloneConfigFile.default() повторяет правила поиска самого rclone:
RCLONE_CONFIG, если задана;%APPDATA%
clone
clone.conf;$XDG_CONFIG_HOME/rclone/rclone.conf, иначе ~/.config/rclone/rclone.conf;~/.rclone.conf — берём его.Зашифрованный файл rclone открывает маркером RCLONE_ENCRYPT_V0: — по нему и определяем,
что потребуется пароль, ещё до запуска rcd.
Два момента, которые легко упустить:
--ask-password=false. Без него процесс с зашифрованным
конфигом просто повиснет, ожидая ввода пароля в stdin, которого у него нет. Пароль
спрашивает GUI и передаёт сам.RCLONE_CONFIG_PASS, а не аргументом
командной строки — аргументы видны в списке процессов любому пользователю системы.Отдельная тонкость: rcd поднимает порт и отвечает на core/version даже тогда, когда
конфиг ему недоступен — расшифровка откладывается до первого обращения к эндпоинтам
config. Поэтому awaitReady() в этом случае успешен, и проверять доступность конфига
нужно явно через RcloneClient.ensureConfigReadable(): он разворачивает отказ
расшифровки в RcloneConfigLockedException, на который GUI реагирует запросом пароля,
а не сообщением “что-то сломалось”.
| Эндпоинт | Назначение |
|---|---|
config/listremotes |
получить список настроенных облаков |
config/get |
детали конкретного облака |
config/create |
добавить новое облако (провайдер + параметры) |
config/update |
изменить параметры |
config/delete |
удалить облако |
mount/mount |
смонтировать remote в точку монтирования |
mount/unmount |
размонтировать |
mount/listmounts |
список активных монтирований |
core/stats |
статистика передачи (для индикатора в трее) |
job/status |
статус долгих операций (например, OAuth-логин) |
rclone mount поверх fuse3. Проверка наличия пакета fuse3/fuse при
первом запуске, подсказка команды установки под обнаруженный дистрибутив.rclone поддерживает --vfs-cache-mode с уровнями:
off — ничего не кэшируется на диск, только потоковое чтение (минимум места, но
нет поддержки seek/перезаписи для некоторых приложений);minimal — кэшируются только открытые на запись файлы;writes — как minimal, плюс более надёжная запись (рекомендуемый режим по умолчанию);full — кэшируются и читаемые файлы (ближе всего к поведению “как обычный диск”).В GUI это будет один переключатель на вкладке настроек каждого подключённого облака.
Десктопная схема (rclone rcd + монтирование через FUSE/WinFsp) на телефонах не применима: Android и iOS не дают приложениям без root/jailbreak примонтировать произвольную ФС, видимую всем остальным приложениям — это осознанное ограничение безопасности обеих платформ, а не то, что можно обойти в коде.
Вместо этого используется общее ядро rclone, но другая “обвязка” сверху:
DocumentsProviderrclone собирается через gomobile bind в .aar-библиотеку — то же ядро
(протоколы облаков, OAuth, кэш), что и на десктопе, без переписывания логики.
Сборку делает .github/workflows/librclone-android.yml
из той же версии rclone, что зафиксирована для десктопа.
Сгенерированный интерфейс — ровно то, подо что заточен RcloneTransport:
public abstract class org.rclone.gomobile.Gomobile {
public static native void rcloneInitialize();
public static native void rcloneFinalize();
public static native RcloneRPCResult rcloneRPC(String method, String input);
}
// RcloneRPCResult: String Output (JSON), int Status (HTTP-код, 200 = успех)
То есть на Android меняется только транспорт: вместо HTTP-запроса к соседнему
процессу — вызов rcloneRPC внутри своего. RcloneClient и весь разбор
ответов переиспользуются как есть.
Про размер. Библиотека большая: 56 МБ в сжатом .aar, внутри по полной
копии rclone на архитектуру (arm64 — 77 МБ, x86_64 — 107 МБ; без -ldflags -s
было бы в полтора раза больше). Поэтому собираются только эти две архитектуры
— первое реальные телефоны, второе эмулятор, — а раздавать приложение придётся
так, чтобы устройство скачивало код лишь своей: через Android App Bundle или
ABI splits. Один APK со всеми архитектурами сразу — не вариант.
android.content.DocumentsProvider (часть Storage Access
Framework). Это официальный и единственный путь для сторонних облачных дисков
на Android — так же устроены Google Drive, Dropbox, Box.ACTION_OPEN_DOCUMENT — не как примонтированная папка, а как
зарегистрированный источник документов.File Provider Extensionrclone, собранное через gomobile bind в .framework для iOS.NSFileProviderExtension
/ FileProviderReplicatedExtension в новых версиях iOS). Это официальный Apple API
для облачных дисков — так работают Dropbox, OneDrive, iCloud Drive.| Платформа | Видимость диска | Механизм | Виден ли произвольным приложениям напрямую |
|---|---|---|---|
| Windows | Буква диска в проводнике | WinFsp | Да, как обычный диск |
| Linux | Точка монтирования | FUSE | Да, как обычная папка |
| macOS | Том в Finder | macFUSE | Да, как обычный том |
| Android | Источник в «Файлах» | DocumentsProvider |
Через системный диалог выбора файла |
| iOS | Источник в «Файлах» | File Provider Extension |
Через системный диалог выбора файла |
opendisk/
├── composeApp/ # GUI-приложение (Compose Multiplatform, десктоп)
│ └── src/
│ ├── commonMain/ # общий код UI и бизнес-логики
│ └── desktopMain/ # платформенные точки входа (Linux/Win/macOS)
├── rclone-bridge/ # обёртка над rclone RC API для десктопа (Этап 1)
├── android/ # DocumentsProvider + UI (появится на Этапе 6)
├── ios/ # File Provider Extension + UI (появится на Этапе 7)
├── docs/ # документация
├── .github/workflows/ # CI (появится на Этапе 5)
├── ROADMAP.md
├── README.md
└── LICENSE