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

11 KiB
Raw Blame History

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

  1. waitOperation: фиксированный sleep 800 мс до 600 с. Нет экспоненциального backoff и отмены. → Правка: backoff 0.5→8 с; прокидывать Task.isCancelled.

  2. filesList(limit:) сканирует только корень (listDirectory("/")), т.е. «все файлы» на самом деле только корневые — функция фактически сломана. → Правка: рекурсивный обход либо убрать вызов до реальной необходимости.

  3. logout() не отзывает токен. Токен остаётся живым в аккаунте Яндекса. → Правка: POST /revoke_token (или подсказка пользователю).

  4. Дублирование кода copy/move — идентичные блоки построения query + 202-обработки. → Правка: общий приватный transfer(path:from:to:overwrite:).

  5. makeRequest не URL-кодирует path внутри query-значений корректно для спецсимволов имён? — URLQueryItem кодирует значения, но пути с +/% стоит прогнать через addingPercentEncoding и проверить тестами на именах с кавычками/эмодзи.

C. UI / состояние — приоритет 3

  1. formattedDate создаёт 2 новых форматтера на каждую строку таблицы. При 500 элементах — 1000 аллокаций на рендер. → Правка: статические кэшированные ISO8601DateFormatter/DateFormatter (static let, одинаковый locale).

  2. ByteFormatter.string создаёт новый ByteCountFormatter на каждый вызов. → Правка: static let.

  3. Триплет colorScheme-вычислений скопирован 3 раза (YanDiskApp, RootView, MenuBarView) + appearance-AppStorage в 4 местах. → Правка: единый AppearanceManager (@Observable) или расширение EnvironmentValues.

  4. Брендовый цвет Color(red: 0.93, green: 0.0, blue: 0.21) захардкожен 4 раза. → Правка: extension Color { static let yandexRed } или Asset ColorSet (он же решает и иконку внутри UI).

  5. selection: Set<String> в Table по path — при обновлении списка выделение молча теряется/остаётся на удалённых путях. → Правка: чистить selection после loadDirectory.

  6. Дублирование UI-состояния логина: showLoginSheet в store + showLogin @State в MenuBarView синхронизируются onChange — два источника правды. → Правка: один источник (store), MenuBarView читает напрямую.

  7. statusMessage никогда не сбрасывается — плашка висит вечно. → Правка: авто-скрытие через Task.sleep(3 c) + анимация.

  8. NsLog-дебаг останется в релизе (YD didFinishLaunching и др.). → Правка: os.Logger с категориями, убрать из release.

  9. Вкладки «Избранное» и «Доступные мне» — заглушки. Избранное сейчас просто показывает корень «/» — дубликат первой вкладки. → Правка: «Избранное» = локальный список избранных путей (UserDefaults, звездочка в контекстном меню), «Доступные мне» — см. блок выше.

  10. NSOpenPanel.runModal() блокирует main thread прямо из тапа тулбара. → Правка: beginSheetModal(for: window) или begin { }.

  11. store.bootstrap() вызывается и из RootView.task, и из MainSplitView.onAppear, и из MenuBarView.onAppear — спасает только didBootstrap-флаг. → Правка: единая точка инициализации.

D. Инфраструктура — приоритет 3

  1. Нет CI. → GitHub Actions-подобный workflow в Gitea (swift build + smoke_test.sh).
  2. .build/ попадёт в репозиторий, если не исключить. → .gitignore: .build/, .DS_Store, *.dSYM.
  3. Client ID захардкожен в DiskAPIClient.swift. Для опенсорса — вынести в конфиг/plist.
  4. 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