documentation rewrite

Signed-off-by: galeon <dertiellis@gmail.com>
This commit is contained in:
galeon committed 2026-10-11 22:32:13 +03:00
1 parent 1943996608
commit f10b654953
34 files changed
+2099 -4576

No files matched your search

+33 -144
View File
@@ -1,165 +1,54 @@
# BuildArchiveWorker
## Обзор класса
`buildarchiveworker.h`, `buildarchiveworker.cpp`
Смена сборки в лаунчере — это перекладывание содержимого `.minecraft`: текущее упаковывается в
архив, папка вычищается, на её место распаковывается архив другой сборки. Речь о гигабайтах модов,
конфигов и миров, и гонять их в потоке GUI нельзя — окно замерзало бы на всё время смены.
Файловые операции над `.minecraft`: упаковать, очистить, распаковать, раскатать пак. В
`.minecraft` могут лежать гигабайты модов и миров, поэтому это единственная часть проекта,
которая работает не в GUI-потоке.
`BuildArchiveWorker` — исполнитель этих операций в отдельном потоке. Он умеет четыре вещи:
упаковать, вычистить, распаковать и докатить пак сезонной сборки поверх уже разложенного
содержимого.
Объект создаёт [BuildSwitcher](BuildSwitcher.md) без родителя, переносит в свой `QThread` и вызывает
слоты через `QMetaObject::invokeMethod(..., Qt::QueuedConnection)`. Работник не знает ни про
сборки, ни про журнал. Он получает пути и отвечает сигналами.
Класс намеренно ничего не знает ни о сборках, ни о путях лаунчера: он принимает готовые пути и
списки. Порядок шагов и восстановление после сбоя — дело [BuildSwitcher](BuildSwitcher.md).
ZIP читается и пишется через `QZipReader` и `QZipWriter` из `Qt6::CorePrivate`.
## Место в проекте и зависимости
## Слоты
Создаётся и целиком управляется [BuildSwitcher](BuildSwitcher.md), который же и переносит его в
собственный `QThread`. Больше к классу никто не обращается.
Каждый слот заканчивается одним `finished(ok, error)`. По ходу работы испускается
`progress(done, total, currentPath)` на каждый файл.
Требования сборки: `Qt6::Core` и `Qt6::CorePrivate` — последний нужен ради `QZipReader` и
`QZipWriter`, которыми читаются и пишутся архивы сборок.
### archive(gameDir, tempZipPath, excludeTop)
## Иерархия и роль
Упаковывает всё содержимое `gameDir`, кроме папок и файлов верхнего уровня из `excludeTop`.
Символьные ссылки пропускаются. Сжатие `AutoCompress`: Qt сам не сжимает то, что уже сжато.
При ошибке или отмене недописанный архив удаляется.
Наследует `QObject`: мета-объектная система, слоты, сигналы и возможность жить в отдельном потоке
через `moveToThread()`. Виртуальных методов базового класса не переопределяет.
### clear(gameDir, keepTop)
Объект живёт в своём `QThread` и вызывается только через очередь сигналов — прямых вызовов его
слотов из потока GUI быть не должно.
Удаляет всё на верхнем уровне `gameDir`, кроме `keepTop`. Символьная ссылка удаляется как файл,
то, на что она указывает, не трогается. Если `gameDir` нет, это успех. Отменить нельзя.
## Публичные методы
### restore(zipPath, gameDir)
#### explicit BuildArchiveWorker(QObject \*parent = nullptr)
Распаковывает архив в `gameDir`. Пути с `..`, абсолютные, с `\` или `:` останавливают распаковку.
Создаёт исполнителя. Конструктор помечен `explicit`.
### applyPack(zipPath, gameDir, removeRelative, forbiddenTop)
#### void requestCancel()
1. Сначала проверяет весь архив. Небезопасный путь или файл внутри общих папок (`forbiddenTop`:
`versions`, `libraries` и т. д.) — отказ до любых изменений. Пак не может подменить jar игры
или библиотеки.
2. Удаляет файлы из `removeRelative`. Опасные пути и пути в общих папках молча пропускаются. После
этого удаляются папки, которые стали пустыми.
3. Распаковывает пак, перезаписывая существующие файлы.
4. Испускает `packEntries(список файлов пака)`, потом `finished(true)`.
Просит прервать текущую операцию. Вызывается из потока GUI прямо во время работы — это
единственный метод класса, предназначенный для вызова снаружи рабочего потока.
## Отмена
Отмена не прерывает операцию мгновенно: признак проверяется между файлами. Хранится он в
`QAtomicInt`, поэтому запись из одного потока и чтение из другого безопасны без блокировок.
`requestCancel()` и `clearCancel()` выставляют атомарный флаг, его можно трогать из любого потока.
Флаг проверяется перед каждым файлом в `archive`, `restore` и `applyPack`.
#### void clearCancel()
Сбрасывает признак отмены перед началом новой операции.
## Публичные слоты
Все четыре слота вызываются через очередь сигналов и выполняются в рабочем потоке. Каждый
завершается сигналом `finished`.
#### void archive(const QString &gameDir, const QString &tempZipPath, const QStringList &excludeTop)
Упаковывает всё содержимое `gameDir` во временный файл `tempZipPath`. Элементы верхнего уровня,
перечисленные в `excludeTop`, в архив не попадают — так из архива сборки исключаются общие
каталоги лаунчера, которые не принадлежат ни одной сборке.
Запись идёт во временный файл, чтобы прерванная упаковка не оставила повреждённый архив на месте
настоящего.
#### void clear(const QString &gameDir, const QStringList &keepTop)
Удаляет из `gameDir` всё, кроме элементов верхнего уровня, перечисленных в `keepTop`. Выполняется
после упаковки, перед распаковкой другой сборки.
#### void restore(const QString &zipPath, const QString &gameDir)
Распаковывает архив сборки в `gameDir`.
#### void applyPack(const QString &zipPath, const QString &gameDir, const QStringList &removeRelative, const QStringList &forbiddenTop)
Докатывает пак сезонной сборки поверх уже разложенного содержимого. Сначала удаляет файлы из
`removeRelative` — те, что ушли из сборки в новой ревизии, — затем распаковывает архив с
перезаписью.
Того, чего нет ни в списке, ни в архиве, операция не касается: миры и скриншоты игрока остаются на
месте. Именно это отличает обновление сезонной сборки от её переустановки.
Параметр `forbiddenTop` — элементы верхнего уровня, которые паку трогать нельзя: общие каталоги
лаунчера. Проверяется здесь, а не только у издателя пака, потому что архив приезжает из сети.
## Сигналы
#### progress(int done, int total, const QString &currentPath)
Ход операции: сколько файлов обработано из скольких и какой обрабатывается сейчас. Испускается по
ходу всех четырёх операций.
Обработчик — `BuildSwitcher` — пересчитывает долю выполнения и обновляет панель прогресса. Сигнал
приходит в поток GUI через очередь, поэтому прямого доступа к состоянию рабочего потока у
обработчика нет.
#### finished(bool ok, const QString &error)
Операция завершена. При `ok == false` во втором параметре — текст ошибки.
Обработчик решает, что делать дальше: перейти к следующему шагу смены сборки или откатить уже
сделанное.
#### packEntries(const QStringList &entries)
Что именно принёс пак — относительными путями. Испускается только из `applyPack()`.
Список сохраняется в описании сезонной сборки: следующему обновлению он нужен, чтобы вычислить,
какие файлы из сборки ушли, и передать их в `removeRelative`.
## Владение и время жизни
Класс наследует `QObject` и принимает `parent`, но на практике родителя не имеет: объект,
перенесённый в другой поток через `moveToThread()`, не может иметь родителя в потоке GUI.
Ответственность за его удаление лежит на `BuildSwitcher`, который создаёт и поток, и исполнителя.
Удалять объект следует безопасным для потоков способом — не напрямую из потока GUI во время
работы. Уничтожение потока раньше исполнителя приведёт к обрыву незавершённой операции.
## Потокобезопасность
Класс рассчитан на жизнь в отдельном потоке. Все четыре слота выполняются в рабочем потоке;
вызывать их напрямую нельзя — только через очередь сигналов.
Единственная точка межпотокового взаимодействия — признак отмены в `QAtomicInt`:
`requestCancel()` и `clearCancel()` пишут его из потока GUI, а рабочий поток читает между файлами.
Другого разделяемого состояния у класса нет, поэтому иных блокировок не требуется.
## Взаимодействие с другими классами
Единственный собеседник — [BuildSwitcher](BuildSwitcher.md). Он создаёт поток и исполнителя,
переносит второй в первый, вызывает слоты через очередь сигналов и принимает `progress`,
`finished` и `packEntries`.
Пути и списки исключений `BuildSwitcher` берёт из [launcherpaths.h](launcherpaths.md) и из
описаний сборок; сам исполнитель к ним не обращается.
## Внешнее взаимодействие
**Файловая система.** Класс читает, пишет и удаляет файлы в папке игры и в каталоге архивов
сборок. Сетевых обращений и дочерних процессов у него нет: архив сезонной сборки к моменту вызова
`applyPack()` уже скачан [SeasonalPackDownloader](SeasonalPackDownloader.md).
Все операции выполняются в рабочем потоке, все сигналы приходят в поток GUI через очередь.
## Пример использования
```cpp
auto *thread = new QThread(this);
auto *worker = new BuildArchiveWorker; // без родителя: уедет в другой поток
worker->moveToThread(thread);
connect(thread, &QThread::finished, worker, &QObject::deleteLater);
thread->start();
connect(worker, &BuildArchiveWorker::progress, this, &Switcher::onProgress);
connect(worker, &BuildArchiveWorker::finished, this, &Switcher::onStepFinished);
worker->clearCancel();
QMetaObject::invokeMethod(worker, "archive", Qt::QueuedConnection,
Q_ARG(QString, gameDir),
Q_ARG(QString, tempZipPath),
Q_ARG(QStringList, excludeTop));
```
Файлы распаковываются через `QFile`, а не `QSaveFile`. Если операцию прервать, последний файл
может остаться недописанным. Поэтому `BuildSwitcher` и не даёт отменить шаги после архивации.
---