Files
YanDisk/ПРАВКИ.md
T
akhmed f2160a7768 v1.0.3: Доступные мне — реальное обнаружение общих папок (поле share/is_owned)
- Models: ShareInfo (rights, is_owned, is_root) + поле share в DiskResource
- DiskAPIClient.sharedWithMe(): обход дерева, сбор ресурсов с is_owned == false
- Вкладка «Доступные мне» показывает общие папки, в контекстном меню права rw/r
- Реестр публичных ссылок из v1.0.2 убран (был workaround)
2026-09-15 22:22:52 +03:00

112 lines
11 KiB
Markdown
Raw Permalink 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 нет эндпоинта «файлы, которыми поделились со мной», и 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<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