- Избранное: правый клик → «В избранное», звезда в строке, метки в UserDefaults, вкладка подгружает ресурсы по путям - Доступные мне: кнопка «+» добавляет публичную ссылку; метаданные через официальный GET /v1/disk/public/resources; открытие/копирование/удаление - Ограничение платформы задокументировано (ПРАВКИ.md): REST API не отдаёт список shared-with-me - smoke_test: фикс пути бандла
112 lines
11 KiB
Markdown
112 lines
11 KiB
Markdown
# 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
|
||
|
||
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<String>`** в 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
|