- Избранное: правый клик → «В избранное», звезда в строке, метки в UserDefaults, вкладка подгружает ресурсы по путям - Доступные мне: кнопка «+» добавляет публичную ссылку; метаданные через официальный GET /v1/disk/public/resources; открытие/копирование/удаление - Ограничение платформы задокументировано (ПРАВКИ.md): REST API не отдаёт список shared-with-me - smoke_test: фикс пути бандла
11 KiB
YanDisk — анализ кода и план правок
Дата: 15.09.2026. Анализ всех исходников Sources/YanDisk/ (~3 500 строк Swift).
Реализовано в этой итерации
| № | Правка | Где |
|---|---|---|
| 1 | Иконка зависит от темы системы: тёмный AppIconDark.icns + переключение Dock-иконки в рантайме по effectiveAppearance (KVO-наблюдатель) |
YanDiskApp.swift, build.sh, scripts/generate_icons.sh |
| 2 | Вкладка «Скриншоты» скрыта из сайдбара (SidebarItem.displayCases); отдельно обсудим реализацию |
AppStore.swift, MainSplitView.swift |
| 3 | Вкладка «Файлы» → «Доступные мне» (файлы, к которым дали доступ другие пользователи). Пока заглушка: публичный REST API Яндекса не отдаёт список «мне поделились» — нужен отдельный план (см. ниже) | AppStore.swift, MainSplitView.swift |
| 4 | Режим синхронизации по умолчанию — «Только в облако» (SyncPolicyStore.globalMode = .cloudOnly) |
SyncEngine.swift |
По пункту 3 — «Доступные мне»
В открытом REST API Яндекс.Диска (cloud-api.yandex.net) нет эндпоинта «файлы, которыми поделились со мной». Проверено эмпирически (15.09.2026): web-API disk.yandex.ru/api/v2 за сессией+капчей (OAuth-токен не принимается, 302 → captcha/passport), в WebDAV и в дереве Диска общие папки не отображаются, в system_folders соответствующего пути нет.
Реализовано (v1.0.2): вкладка «Доступные мне» = реестр публичных ссылок, добавленных пользователем (кнопка «+»); метаданные тянутся официальным GET /v1/disk/public/resources?public_key=…, открытие в браузере, копирование и удаление ссылки из реестра. Хранилище — UserDefaults (PublicLinks).
Найденные неоптимальные решения и план правок
A. Синхронизация (SyncEngine) — приоритет 1
-
uploadLocalChanges— N+1 запросов. Для каждого локального файла делается отдельныйGET /resources(existsRemotely). При 100 файлах = 100 запросов за цикл синхронизации. → Правка: сравнивать с уже полученным спискомlistDirectory(он уже есть вsyncDirectory), а не спрашивать API по каждому файлу. -
Полное перестроение дерева каждые 25 секунд. Таймер дергает
syncNow(), который рекурсивно обходит всё облако (до глубины 6) независимо от изменений. → Правка: долгий poll черезGET /disk/+revision/ETag; синк дерева только при изменении revision. Интервал с backoff (25 c → 60 c при отсутствии активности). -
Нет chunked-загрузки и прогресса.
uploadFileгрузит файл целиком одним PUT;progressвdownloadFileобъявлен, но нигде не используется;TransferTask.progressвсегда 0 до завершения. → Правка: резюмабельные загрузки через Range, прогресс черезProgress/session.downloaddelegate; большие файлы (>1 ГБ) — черезupload/create+ PUT по частям. -
finishLastTaskпомечаетtasks[0]— гонка. При параллельных операциях помечается не та задача; задачи upload/download из разных потоков перемешиваются. → Правка:appendTaskвозвращаетUUID, операция закрывает задачу по своему id. -
Сравнение файлов только по размеру (
localSize != remoteSize, localSize < remoteSize) — переименование контента без смены размера не ловится; md5 не используется, хотя приходит в API. → Правка: сравнение поmd5для файлов < N МБ, по размеру+mtime для остальных. -
Удаления из облака не отражаются локально (кроме cloudOnly-ветки): если файл исчез с Диска, локальная копия остаётся. И наоборот:
computerOnlyвmode(for:)не блокирует скачивание в веткеcase .computerOnly, .everywhere: break. → Правка: явная семантика режимов:cloudOnly= удалить локальное,everywhere= полное зеркало (в т.ч. удаления),computerOnly= скачать и не следить за удалением в облаке. -
depth < 6молча обрывает глубокие деревья. → Правка: убрать ограничение, ограничивать только количеством API-запросов в минуту.
B. Клиент API (DiskAPIClient) — приоритет 2
-
waitOperation: фиксированный sleep 800 мс до 600 с. Нет экспоненциального backoff и отмены. → Правка: backoff 0.5→8 с; прокидыватьTask.isCancelled. -
filesList(limit:)сканирует только корень (listDirectory("/")), т.е. «все файлы» на самом деле только корневые — функция фактически сломана. → Правка: рекурсивный обход либо убрать вызов до реальной необходимости. -
logout()не отзывает токен. Токен остаётся живым в аккаунте Яндекса. → Правка:POST /revoke_token(или подсказка пользователю). -
Дублирование кода copy/move — идентичные блоки построения query + 202-обработки. → Правка: общий приватный
transfer(path:from:to:overwrite:). -
makeRequestне URL-кодирует path внутри query-значений корректно для спецсимволов имён? —URLQueryItemкодирует значения, но пути с+/%стоит прогнать черезaddingPercentEncodingи проверить тестами на именах с кавычками/эмодзи.
C. UI / состояние — приоритет 3
-
formattedDateсоздаёт 2 новых форматтера на каждую строку таблицы. При 500 элементах — 1000 аллокаций на рендер. → Правка: статические кэшированныеISO8601DateFormatter/DateFormatter(static let, одинаковый locale). -
ByteFormatter.stringсоздаёт новыйByteCountFormatterна каждый вызов. → Правка: static let. -
Триплет
colorScheme-вычислений скопирован 3 раза (YanDiskApp, RootView, MenuBarView) +appearance-AppStorage в 4 местах. → Правка: единыйAppearanceManager(@Observable) или расширениеEnvironmentValues. -
Брендовый цвет
Color(red: 0.93, green: 0.0, blue: 0.21)захардкожен 4 раза. → Правка:extension Color { static let yandexRed }или Asset ColorSet (он же решает и иконку внутри UI). -
selection: Set<String>в Table по path — при обновлении списка выделение молча теряется/остаётся на удалённых путях. → Правка: чистить selection послеloadDirectory. -
Дублирование UI-состояния логина:
showLoginSheetв store +showLogin@State в MenuBarView синхронизируются onChange — два источника правды. → Правка: один источник (store), MenuBarView читает напрямую. -
statusMessageникогда не сбрасывается — плашка висит вечно. → Правка: авто-скрытие через Task.sleep(3 c) + анимация. -
NsLog-дебаг останется в релизе (YD didFinishLaunchingи др.). → Правка:os.Loggerс категориями, убрать из release. -
Вкладки «Избранное» и «Доступные мне» — заглушки. Избранное сейчас просто показывает корень «/» — дубликат первой вкладки. → Правка: «Избранное» = локальный список избранных путей (UserDefaults, звездочка в контекстном меню), «Доступные мне» — см. блок выше.
-
NSOpenPanel.runModal()блокирует main thread прямо из тапа тулбара. → Правка:beginSheetModal(for: window)илиbegin { }. -
store.bootstrap()вызывается и изRootView.task, и изMainSplitView.onAppear, и изMenuBarView.onAppear— спасает толькоdidBootstrap-флаг. → Правка: единая точка инициализации.
D. Инфраструктура — приоритет 3
- Нет CI. → GitHub Actions-подобный workflow в Gitea (swift build + smoke_test.sh).
.build/попадёт в репозиторий, если не исключить. →.gitignore:.build/,.DS_Store,*.dSYM.- Client ID захардкожен в
DiskAPIClient.swift. Для опенсорса — вынести в конфиг/plist. smoke_test.sh— проверить, что он реально гоняется после каждого билда; включить в CI.
Порядок работ (предложение)
- B8, A1, A4, A5 (корректность синка)
- A2 (экономия API), A6 (семантика режимов)
- C13, C14, C15, C16 (перф/чистка — дешёвые)
- C21 «Избранное» локально, C19, C22
- «Доступные мне» — после выбора подхода
- D24–D27