# SeasonalPackDownloader

## Обзор класса

`SeasonalPackDownloader` скачивает один архив сезонной сборки в файл. Задача узкая и отдельная по
двум причинам: пак — это сотни мегабайт, поэтому он пишется потоком, а не держится в памяти; и его
sha256 обязательно сверяется, потому что распаковывать битую загрузку поверх рабочей `.minecraft`
нельзя.

Распаковкой класс не занимается — это делает [BuildSwitcher](BuildSwitcher.md) в отдельном потоке
вместе с остальными операциями над содержимым папки игры.

Набор геттеров прогресса повторяет [VersionInstaller](VersionInstaller.md) и
[JavaInstaller](JavaInstaller.md): панель загрузки в интерфейсе читает их одинаково, независимо от
того, кто сейчас работает.

## Место в проекте и зависимости

Экземпляр создаётся и принадлежит [LauncherBackend](LauncherBackend.md). Адрес архива, его размер
и контрольную сумму даёт запись каталога от [SeasonalBuildService](SeasonalBuildService.md).

Требования сборки: `Qt6::Core` (`QCryptographicHash`, `QSaveFile`, `QTimer`) и `Qt6::Network`
(`QNetworkAccessManager`, `QNetworkReply`).

## Иерархия и роль

Наследует `QObject`: мета-объектная система, пять сигналов и владение по родителю. Объявлен
виртуальный деструктор — класс владеет незавершённой загрузкой и открытым файлом.

## Публичные методы

#### explicit SeasonalPackDownloader(QObject \*parent = nullptr)

Создаёт загрузчик, его `QNetworkAccessManager` и таймер сглаживания прогресса. Конструктор помечен
`explicit`.

#### bool isRunning() const

Идёт ли загрузка прямо сейчас.

#### QString label() const

Подпись загрузки для интерфейса — как правило, название сезонной сборки.

#### QString stage() const

Текущий этап словами.

#### QString currentFile() const

Файл, который качается сейчас.

#### qint64 bytesDone() const

Сколько байт уже получено.

#### qint64 bytesTotal() const

Ожидаемый размер архива.

#### double fraction() const

Доля выполнения от `0` до `1` либо `-1`, пока итог неизвестен — например, когда сервер не сообщил
размер, а в записи каталога он не был указан.

#### void download(const QUrl &url, const QString &targetPath, const QString &sha256, qint64 expectedSize, const QString &label)

Скачивает архив по адресу `url` в `targetPath`, сверяя sha256 с переданным значением.

Файл `targetPath` перезаписывается: недокачанный пак с прошлой попытки не должен пережить новую.
Запись идёт через `QSaveFile`, поэтому на месте назначения файл появляется только целиком и только
после успешной проверки контрольной суммы.

Параметр `expectedSize` берётся из записи каталога и используется для расчёта доли выполнения,
пока сервер не сообщил размер сам.

#### void cancel()

Отменяет загрузку. Недокачанный файл на месте назначения не остаётся.

## Сигналы

#### started(const QString &label)

Загрузка началась. Обработчик показывает панель прогресса.

#### progressChanged()

Изменились числа прогресса; испускается не чаще, чем позволяет внутренний таймер, — иначе сигнал
на каждый принятый блок обошёлся бы дороже самой загрузки. Обработчик перечитывает геттеры.

#### finished(const QString &path)

Архив скачан и проверен; в параметре — путь к готовому файлу.

Обработчик передаёт этот путь [BuildSwitcher](BuildSwitcher.md) для раскатки поверх содержимого
`.minecraft`.

#### failed(const QString &label, const QString &message)

Загрузка не удалась: сеть недоступна, сервер ответил ошибкой или не сошлась контрольная сумма.
Последний случай особенно важен — он означает, что архив повреждён и распаковывать его нельзя.

#### canceled(const QString &label)

Загрузка отменена пользователем.

## Владение и время жизни

Класс наследует `QObject` и принимает `parent` — родитель его и удалит.
`QNetworkAccessManager` и таймер создаются в конструкторе с загрузчиком в роли родителя.

Скачиваемый файл хранится как `std::unique_ptr<QSaveFile>`: незавершённая запись отменяется вместе
с уничтожением объекта, и повреждённый архив не попадает на место назначения. Сетевой ответ
создаётся по ходу работы и закрывается в деструкторе.

## Потокобезопасность

Только поток GUI. Данные пишутся на диск блоками по мере поступления, поэтому длительных
синхронных операций в потоке нет.

## Взаимодействие с другими классами

`LauncherBackend` вызывает `download()` при установке или обновлении сезонной сборки, передавая
адрес и контрольную сумму из записи [SeasonalBuildService](SeasonalBuildService.md). Сигналы
прогресса он переправляет в те же свойства, что и остальные загрузчики, поэтому панель в главном
окне не различает, кто работает.

По сигналу `finished` бэкенд передаёт путь к архиву в `BuildSwitcher::applyPack()` вместе со
списком уходящих файлов из описания предыдущей ревизии.

## Внешнее взаимодействие

**Сеть, исходящие запросы.** Одна загрузка по HTTPS с файлового сервера сборок. Направление
одностороннее, тело ответа — двоичный архив.

Данные пишутся потоком через `QSaveFile` с одновременным подсчётом sha256; несовпадение суммы
приводит к сигналу `failed`, и файл на месте назначения не появляется. Повторных попыток класс не
делает: решение о повторе принимает пользователь.

Все сигналы приходят в поток GUI.

## Пример использования

```cpp
auto *packLoader = new SeasonalPackDownloader(this);

connect(packLoader, &SeasonalPackDownloader::progressChanged, this, &Backend::downloadChanged);
connect(packLoader, &SeasonalPackDownloader::finished, this, [this](const QString &path) {
    m_switcher->applyPack(m_buildId, m_buildName, path,
                          m_previousEntries, m_note, m_gameDir);
});
connect(packLoader, &SeasonalPackDownloader::failed, this,
        [this](const QString &, const QString &message) { emit launchError(message); });

packLoader->download(entry.archiveUrl, targetPath,
                     entry.archiveSha256, entry.archiveSize, entry.name);
```

---

При создании этого документа использовался ИИ.
