# 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 нет эндпоинта «файлы, которыми поделились со мной», и web-API `disk.yandex.ru/api/v2` требует браузерную сессию (302 → капча). **Найдено и реализовано (v1.0.3):** в ответе `GET /v1/disk/resources` (листинг любого каталога) каждый ресурс несёт недокументированное поле `share: {rights, is_owned, is_root}`. Папки, к которым доступ дали другие пользователи, лежат прямо в дереве Диска с `is_owned: false`. `DiskAPIClient.sharedWithMe()` обходит дерево (до глубины 3, внутрь чужих общих корней не заходит) и собирает все ресурсы с `is_owned == false`. Вкладка «Доступные мне» показывает их с правами (`rw`/`r`) в контекстном меню. --- ## Найденные неоптимальные решения и план правок ### A. Синхронизация (SyncEngine) — приоритет 1 1. **`uploadLocalChanges` — N+1 запросов.** Для каждого локального файла делается отдельный `GET /resources` (`existsRemotely`). При 100 файлах = 100 запросов за цикл синхронизации. → Правка: сравнивать с уже полученным списком `listDirectory` (он уже есть в `syncDirectory`), а не спрашивать API по каждому файлу. 2. **Полное перестроение дерева каждые 25 секунд.** Таймер дергает `syncNow()`, который рекурсивно обходит всё облако (до глубины 6) независимо от изменений. → Правка: долгий poll через `GET /disk/` + `revision`/ETag; синк дерева только при изменении revision. Интервал с backoff (25 c → 60 c при отсутствии активности). 3. **Нет chunked-загрузки и прогресса.** `uploadFile` грузит файл целиком одним PUT; `progress` в `downloadFile` объявлен, но нигде не используется; `TransferTask.progress` всегда 0 до завершения. → Правка: резюмабельные загрузки через Range, прогресс через `Progress`/`session.download` delegate; большие файлы (>1 ГБ) — через `upload/create` + PUT по частям. 4. **`finishLastTask` помечает `tasks[0]` — гонка.** При параллельных операциях помечается не та задача; задачи upload/download из разных потоков перемешиваются. → Правка: `appendTask` возвращает `UUID`, операция закрывает задачу по своему id. 5. **Сравнение файлов только по размеру** (`localSize != remoteSize, localSize < remoteSize`) — переименование контента без смены размера не ловится; md5 не используется, хотя приходит в API. → Правка: сравнение по `md5` для файлов < N МБ, по размеру+mtime для остальных. 6. **Удаления из облака не отражаются локально** (кроме cloudOnly-ветки): если файл исчез с Диска, локальная копия остаётся. И наоборот: `computerOnly` в `mode(for:)` не блокирует скачивание в ветке `case .computerOnly, .everywhere: break`. → Правка: явная семантика режимов: `cloudOnly` = удалить локальное, `everywhere` = полное зеркало (в т.ч. удаления), `computerOnly` = скачать и не следить за удалением в облаке. 7. **`depth < 6` молча обрывает** глубокие деревья. → Правка: убрать ограничение, ограничивать только количеством API-запросов в минуту. ### B. Клиент API (DiskAPIClient) — приоритет 2 8. **`waitOperation`: фиксированный sleep 800 мс до 600 с.** Нет экспоненциального backoff и отмены. → Правка: backoff 0.5→8 с; прокидывать `Task.isCancelled`. 9. **`filesList(limit:)` сканирует только корень** (`listDirectory("/")`), т.е. «все файлы» на самом деле только корневые — функция фактически сломана. → Правка: рекурсивный обход либо убрать вызов до реальной необходимости. 10. **`logout()` не отзывает токен.** Токен остаётся живым в аккаунте Яндекса. → Правка: `POST /revoke_token` (или подсказка пользователю). 11. **Дублирование кода copy/move** — идентичные блоки построения query + 202-обработки. → Правка: общий приватный `transfer(path:from:to:overwrite:)`. 12. **`makeRequest` не URL-кодирует path** внутри query-значений корректно для спецсимволов имён? — `URLQueryItem` кодирует значения, но пути с `+`/`%` стоит прогнать через `addingPercentEncoding` и проверить тестами на именах с кавычками/эмодзи. ### C. UI / состояние — приоритет 3 13. **`formattedDate` создаёт 2 новых форматтера на каждую строку таблицы.** При 500 элементах — 1000 аллокаций на рендер. → Правка: статические кэшированные `ISO8601DateFormatter`/`DateFormatter` (static let, одинаковый locale). 14. **`ByteFormatter.string` создаёт новый `ByteCountFormatter` на каждый вызов.** → Правка: static let. 15. **Триплет `colorScheme`-вычислений скопирован 3 раза** (YanDiskApp, RootView, MenuBarView) + `appearance`-AppStorage в 4 местах. → Правка: единый `AppearanceManager` (@Observable) или расширение `EnvironmentValues`. 16. **Брендовый цвет `Color(red: 0.93, green: 0.0, blue: 0.21)` захардкожен 4 раза.** → Правка: `extension Color { static let yandexRed }` или Asset ColorSet (он же решает и иконку внутри UI). 17. **`selection: Set`** в Table по path — при обновлении списка выделение молча теряется/остаётся на удалённых путях. → Правка: чистить selection после `loadDirectory`. 18. **Дублирование UI-состояния логина:** `showLoginSheet` в store + `showLogin` @State в MenuBarView синхронизируются onChange — два источника правды. → Правка: один источник (store), MenuBarView читает напрямую. 19. **`statusMessage` никогда не сбрасывается** — плашка висит вечно. → Правка: авто-скрытие через Task.sleep(3 c) + анимация. 20. **`NsLog`-дебаг останется в релизе** (`YD didFinishLaunching` и др.). → Правка: `os.Logger` с категориями, убрать из release. 21. **Вкладки «Избранное» и «Доступные мне» — заглушки.** Избранное сейчас просто показывает корень «/» — дубликат первой вкладки. → Правка: «Избранное» = локальный список избранных путей (UserDefaults, звездочка в контекстном меню), «Доступные мне» — см. блок выше. 22. **`NSOpenPanel.runModal()` блокирует main thread** прямо из тапа тулбара. → Правка: `beginSheetModal(for: window)` или `begin { }`. 23. **`store.bootstrap()` вызывается и из `RootView.task`, и из `MainSplitView.onAppear`, и из `MenuBarView.onAppear`** — спасает только `didBootstrap`-флаг. → Правка: единая точка инициализации. ### D. Инфраструктура — приоритет 3 24. **Нет CI.** → GitHub Actions-подобный workflow в Gitea (swift build + smoke_test.sh). 25. **`.build/` попадёт в репозиторий**, если не исключить. → `.gitignore`: `.build/`, `.DS_Store`, `*.dSYM`. 26. **Client ID захардкожен** в `DiskAPIClient.swift`. Для опенсорса — вынести в конфиг/plist. 27. **`smoke_test.sh`** — проверить, что он реально гоняется после каждого билда; включить в CI. ## Порядок работ (предложение) 1. B8, A1, A4, A5 (корректность синка) 2. A2 (экономия API), A6 (семантика режимов) 3. C13, C14, C15, C16 (перф/чистка — дешёвые) 4. C21 «Избранное» локально, C19, C22 5. «Доступные мне» — после выбора подхода 6. D24–D27