Files
YanDisk/ПРАВКИ.md
T
akhmed 9098340f2a YanDisk v1.0.1: тёмная иконка по теме системы, вкладки (Скриншоты скрыт, Файлы→Доступные мне), синк по умолчанию 'В облаке', ПРАВКИ.md
- Иконка: AppIconDark.icns + переключение Dock-иконки по effectiveAppearance (KVO)
- Сайдбар: убран 'Скриншоты', 'Файлы' → 'Доступные мне' (заглушка), первая вкладка — все файлы и папки
- SyncPolicyStore.globalMode по умолчанию .cloudOnly ('В облаке')
- build.sh/smoke_test.sh: SDKROOT из Xcode (фикс SDK-конфликта тулчейна 6.3.3/SDK 6.4)
- ПРАВКИ.md: полный аудит и план доработок
2026-09-15 21:16:22 +03:00

114 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`) нет эндпоинта «файлы, которыми поделились со мной». Варианты реализации (по убыванию реалистичности):
1. Квази-«Доступные мне»: список ресурсов с `public_url` + собственный реестр входящих ссылок (пользователь добавляет публичную ссылку в приложение; монтируем в виртуальную папку через `/public/resources`).
2. Неофициальный web-API disk.yandex.ru (папка «Общий доступ») — хрупко, зависит от внутренней авторизации.
3. Вкладка «Приглашения» через веб-интерфейс.
Решение о выборе — отдельно.
---
## Найденные неоптимальные решения и план правок
### 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