This commit is contained in:
2026-09-29 09:40:37 +03:00
parent 441021b9a6
commit 6567e731d1
34 changed files with 57 additions and 709 deletions
+1 -54
View File
@@ -22,13 +22,8 @@ const QString kIndexFile = QStringLiteral("index.json");
const QString kStateSwitching = QStringLiteral("switching");
const QString kStateApplying = QStringLiteral("applying");
const QString kStateIdle = QStringLiteral("idle");
// Запас на архив: точный размер заранее неизвестен, но упасть на середине
// записи из-за полного диска хуже, чем отказаться заранее.
constexpr qint64 kFreeSpaceSlackBytes = 256LL * 1024 * 1024;
// Имя файла из названия сборки: оно попадает в файловую систему, поэтому всё
// небезопасное заменяется подчёркиванием.
QString slug(const QString &name)
{
QString result;
@@ -68,14 +63,10 @@ qint64 directorySize(const QString &path, const QStringList &excludeTop)
return total;
}
} // namespace
}
QStringList BuildSwitcher::sharedTopLevel()
{
// Общий кэш, одинаковый для всех сборок и всегда восстановимый загрузкой.
// versions/ здесь не случайно: без libraries/ описания версий бесполезны, а
// две сборки на одной версии игры иначе таскали бы по копии клиентского jar.
// Чем сборка запускается, помнит resolvedVersionId, а не набор папок.
return {QStringLiteral("assets"),
QStringLiteral("libraries"),
QStringLiteral("versions"),
@@ -118,13 +109,9 @@ QString BuildSwitcher::buildDir(int buildId) const
return LauncherPaths::buildDir(buildId);
}
// На сборку — ровно один архив: имя с датой меняется от переключения к
// переключению, поэтому ищем по расширению, а не по имени.
QString BuildSwitcher::archivePathOf(int buildId) const
{
const QDir dir(buildDir(buildId));
// Рядом лежат служебные .tmp-*.zip и .pack-*.zip. На Unix они скрытые и в
// выборку не попадают, на Windows — попадают, поэтому отсекаем по имени.
const QStringList zips = dir.entryList({QStringLiteral("*.zip")}, QDir::Files, QDir::Time);
for (const QString &name : zips) {
if (!name.startsWith(u'.'))
@@ -165,7 +152,6 @@ void BuildSwitcher::switchTo(int fromBuildId,
m_total = 0;
m_worker->clearCancel();
// Первая сборка за всё время: архивировать нечего, сразу разворачиваем цель.
if (fromBuildId < 0 || !QDir(gameDir).exists()) {
m_state = Clearing;
beginClearing();
@@ -201,8 +187,6 @@ void BuildSwitcher::switchTo(int fromBuildId,
void BuildSwitcher::cancel()
{
// Отмена имеет смысл только на архивации: после очистки папки отступать
// уже некуда, там нужно доводить дело до конца.
if (m_state == Archiving)
m_worker->requestCancel();
}
@@ -260,8 +244,6 @@ void BuildSwitcher::onWorkerFinished(bool ok, const QString &error)
return;
case Clearing:
if (!ok) {
// Папка осталась разобранной: активной сборкой считается прежняя,
// отметка о незавершённом переключении в index.json остаётся.
fail(error, /*gameDirIntact=*/false);
return;
}
@@ -276,10 +258,6 @@ void BuildSwitcher::onWorkerFinished(bool ok, const QString &error)
return;
case Applying:
if (!ok) {
// Архив проверяется целиком до первой записи, поэтому нулевой
// счётчик означает, что в папке игры ничего не тронуто. Иначе
// часть файлов уже заменена: отметка в index.json остаётся, и
// следующий запуск доиграет раскатку из сохранённого пака.
fail(error, /*gameDirIntact=*/m_done == 0);
return;
}
@@ -294,7 +272,6 @@ void BuildSwitcher::commitArchive()
+ QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd"))
+ u'_' + slug(m_fromName) + QStringLiteral(".zip");
// На сборку — один архив: новый встаёт на место, все прежние уходят.
if (QFile::exists(target) && !QFile::remove(target)) {
QFile::remove(m_tempZip);
fail(Loc::text("switch.error.archiveReplaceFailed").arg(QDir::toNativeSeparators(target)), true);
@@ -309,14 +286,10 @@ void BuildSwitcher::commitArchive()
const QDir dir(buildDir(m_fromId));
const QStringList zips = dir.entryList({QStringLiteral("*.zip")}, QDir::Files);
for (const QString &name : zips) {
// Служебные файлы под удаление не попадают: в .pack-*.zip может лежать
// пак недоигранной раскатки, без которого её нечем довести до конца.
if (!name.startsWith(u'.') && dir.absoluteFilePath(name) != target)
QFile::remove(dir.absoluteFilePath(name));
}
// Отметка ставится до первого разрушающего действия: по ней следующий
// запуск поймёт, что переключение оборвалось на середине.
QJsonObject index = readIndex();
index.insert(QStringLiteral("state"), kStateSwitching);
index.insert(QStringLiteral("from"), m_fromId);
@@ -344,8 +317,6 @@ void BuildSwitcher::beginRestoring()
{
const QString archive = archivePathOf(m_toId);
if (archive.isEmpty()) {
// Сборка ещё ни разу не запускалась: пустая .minecraft рядом с общими
// кэшами — это и есть её правильное начальное состояние.
m_state = Restoring;
finish();
return;
@@ -356,8 +327,6 @@ void BuildSwitcher::beginRestoring()
requestRestore(archive, m_gameDir);
}
// ── Раскатка пака сезонной сборки ──────────────────────────────────────────
void BuildSwitcher::applyPack(int buildId,
const QString &buildName,
const QString &packZipPath,
@@ -396,8 +365,6 @@ void BuildSwitcher::applyPack(int buildId,
m_total = 0;
m_worker->clearCancel();
// Отметка ставится до первого разрушающего действия — как и при смене
// сборки. Пак лежит рядом и переживёт перезапуск вместе с ней.
QJsonObject index = readIndex();
index.insert(QStringLiteral("state"), kStateApplying);
index.insert(QStringLiteral("to"), buildId);
@@ -419,8 +386,6 @@ void BuildSwitcher::beginApplying()
void BuildSwitcher::finishApplying()
{
// Пак сделал своё дело: держать сотни мегабайт рядом с архивом сборки не
// за чем, а его наличие означало бы недоигранную раскатку.
QFile::remove(m_packPath);
QJsonObject index = readIndex();
@@ -477,16 +442,11 @@ void BuildSwitcher::fail(const QString &message, bool gameDirIntact)
emit failed(toId, message, gameDirIntact);
}
// ── Удаление сборки ────────────────────────────────────────────────────────
bool BuildSwitcher::forgetBuild(int buildId)
{
if (buildId < 0)
return true;
// Запись убираем в любом случае: даже если файл не удалось стереть, ссылаться
// на архив несуществующей сборки нельзя — иначе он «воскреснет» под новой
// сборкой с тем же id.
QJsonObject index = readIndex();
QJsonObject builds = index.value(QStringLiteral("builds")).toObject();
builds.remove(QString::number(buildId));
@@ -499,8 +459,6 @@ bool BuildSwitcher::forgetBuild(int buildId)
return !dir.exists() || dir.removeRecursively();
}
// ── Незавершённое переключение ─────────────────────────────────────────────
QString BuildSwitcher::interruptedSwitchWarning() const
{
const QJsonObject index = readIndex();
@@ -540,8 +498,6 @@ void BuildSwitcher::resumeInterrupted(const QString &gameDir)
if (toId < 0 || m_state != Idle)
return;
// Архив исходной сборки уже записан целиком — доигрываем только вторую
// половину: очистку и распаковку цели.
m_fromId = -1;
m_toId = toId;
m_toName = index.value(QStringLiteral("builds"))
@@ -566,10 +522,6 @@ void BuildSwitcher::resumeApplying(const QJsonObject &index, const QString &game
const QString packName = index.value(QStringLiteral("pack")).toString();
const QString packPath = buildDir(toId) + u'/' + packName;
if (packName.isEmpty() || packName.contains(u'/') || !QFile::exists(packPath)) {
// Пак не сохранился — доигрывать нечем. Отметку снимаем, иначе
// предупреждение о незавершённой установке будет всплывать вечно;
// сборка при этом остаётся наполовину обновлённой, и лаунчер покажет
// её как требующую обновления.
QJsonObject cleaned = index;
cleaned.insert(QStringLiteral("state"), kStateIdle);
cleaned.remove(QStringLiteral("pack"));
@@ -601,8 +553,6 @@ void BuildSwitcher::resumeApplying(const QJsonObject &index, const QString &game
beginApplying();
}
// ── Реестр архивов ─────────────────────────────────────────────────────────
QJsonObject BuildSwitcher::readIndex() const
{
QFile file(LauncherPaths::buildStorageDir() + u'/' + kIndexFile);
@@ -626,9 +576,6 @@ void BuildSwitcher::writeIndex(const QJsonObject &index) const
file.commit();
}
// Оборванная запись архива оставляет .tmp-*.zip: он никому не нужен и может
// занимать гигабайты. Пакеты .pack-*.zip не трогаем: по ним доигрывается
// прерванная раскатка сезонной сборки, а лишний тут удаляет resumeApplying.
void BuildSwitcher::sweepTempArchives() const
{
QDir root(LauncherPaths::buildStorageDir());