Files
YanDisk/ПРАВКИ.md
T
akhmed 19e8360712 v1.0.2: Избранное (метка + вкладка), Доступные мне (реестр публичных ссылок)
- Избранное: правый клик → «В избранное», звезда в строке, метки в UserDefaults, вкладка подгружает ресурсы по путям
- Доступные мне: кнопка «+» добавляет публичную ссылку; метаданные через официальный GET /v1/disk/public/resources; открытие/копирование/удаление
- Ограничение платформы задокументировано (ПРАВКИ.md): REST API не отдаёт список shared-with-me
- smoke_test: фикс пути бандла
2026-09-15 22:12:49 +03:00

112 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`) нет эндпоинта «файлы, которыми поделились со мной». Проверено эмпирически (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