2024-07-28 11:17:30 -04:00
|
|
|
// SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
|
|
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
|
|
|
|
#include "charactersync.h"
|
|
|
|
|
|
|
|
#include <KLocalizedString>
|
|
|
|
#include <KZip>
|
2024-07-30 20:41:35 -04:00
|
|
|
#include <qcorosignal.h>
|
|
|
|
#include <qcorotask.h>
|
2024-07-28 11:17:30 -04:00
|
|
|
|
|
|
|
#include "astra_log.h"
|
|
|
|
#include "syncmanager.h"
|
|
|
|
|
2024-07-30 20:41:35 -04:00
|
|
|
const auto gearsetFilename = QStringLiteral("GEARSET.DAT");
|
|
|
|
|
2024-07-28 11:17:30 -04:00
|
|
|
CharacterSync::CharacterSync(Account &account, LauncherCore &launcher, QObject *parent)
|
2024-07-30 20:41:35 -04:00
|
|
|
: QObject(parent)
|
|
|
|
, launcher(launcher)
|
2024-07-28 11:17:30 -04:00
|
|
|
, m_account(account)
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
2024-07-30 19:47:26 -04:00
|
|
|
QCoro::Task<bool> CharacterSync::sync(const bool initialSync)
|
2024-07-28 11:17:30 -04:00
|
|
|
{
|
|
|
|
if (!launcher.settings()->enableSync()) {
|
|
|
|
co_return true;
|
|
|
|
}
|
|
|
|
|
2024-07-30 20:41:35 -04:00
|
|
|
const auto syncManager = launcher.syncManager();
|
2024-07-28 11:17:30 -04:00
|
|
|
if (!syncManager->connected()) {
|
|
|
|
// TODO: provide an option to continue in the UI
|
|
|
|
Q_EMIT launcher.loginError(i18n("Failed to connect to sync server! Please check your sync settings."));
|
|
|
|
co_return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!syncManager->isReady()) {
|
|
|
|
Q_EMIT launcher.stageChanged(i18n("Waiting for sync connection..."));
|
|
|
|
|
|
|
|
// NOTE: probably does not handle errors well?
|
|
|
|
co_await qCoro(syncManager, &SyncManager::isReadyChanged);
|
|
|
|
}
|
|
|
|
|
|
|
|
Q_EMIT launcher.stageChanged(i18n("Synchronizing character data..."));
|
|
|
|
|
2024-07-30 20:07:24 -04:00
|
|
|
// Perform a manual sync just in case
|
|
|
|
co_await syncManager->sync();
|
|
|
|
|
2024-07-30 19:47:26 -04:00
|
|
|
// On game boot, check if we need the lock. Otherwise break it when we clean up.
|
|
|
|
if (initialSync) {
|
|
|
|
if (const auto hostname = co_await syncManager->checkLock(); hostname.has_value()) {
|
|
|
|
// Don't warn about our own failures
|
|
|
|
if (hostname != QSysInfo::machineHostName()) {
|
|
|
|
Q_EMIT launcher.loginError(i18n("Device %1 has not yet uploaded it's character data. Astra will not continue until that device is re-synced."));
|
|
|
|
co_return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
syncManager->setLock();
|
|
|
|
} else {
|
|
|
|
syncManager->breakLock();
|
|
|
|
}
|
|
|
|
|
2024-07-28 11:17:30 -04:00
|
|
|
// so first, we need to list the character folders
|
|
|
|
// we sync each one separately
|
|
|
|
QList<QFileInfo> characterDirs;
|
|
|
|
|
|
|
|
const QDir configPath = m_account.getConfigPath();
|
|
|
|
qCDebug(ASTRA_LOG) << "Searching for characters in" << configPath;
|
|
|
|
|
|
|
|
QDirIterator configIterator(configPath.absolutePath());
|
|
|
|
while (configIterator.hasNext()) {
|
|
|
|
const auto fileInfo = configIterator.nextFileInfo();
|
|
|
|
if (fileInfo.isDir() && fileInfo.fileName().startsWith(QStringLiteral("FFXIV_"))) {
|
|
|
|
characterDirs.append(fileInfo);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
qCDebug(ASTRA_LOG) << "Character directories:" << characterDirs;
|
|
|
|
|
|
|
|
for (const auto &dir : characterDirs) {
|
|
|
|
const QString id = dir.fileName(); // FFXIV_CHR0040000001000001 for example
|
|
|
|
const auto previousData = co_await syncManager->getUploadedCharacterData(id);
|
2024-07-30 19:47:26 -04:00
|
|
|
|
2024-07-30 20:41:35 -04:00
|
|
|
// The files are packed into an archive. So if only one of the files doesn't exist or fails the hash check, download the whole thing and overwrite.
|
|
|
|
bool areFilesDifferent = false;
|
|
|
|
for (const auto &[file, hash] : previousData->fileHashes.asKeyValueRange()) {
|
|
|
|
QFile existingFile(QDir(dir.absoluteFilePath()).absoluteFilePath(file));
|
|
|
|
if (!existingFile.exists()) {
|
|
|
|
areFilesDifferent = true;
|
|
|
|
qCDebug(ASTRA_LOG) << id << "does not match locally, reason:" << existingFile.fileName() << "does not exist";
|
|
|
|
break;
|
|
|
|
}
|
2024-07-30 19:47:26 -04:00
|
|
|
|
2024-07-30 20:41:35 -04:00
|
|
|
existingFile.open(QIODevice::ReadOnly);
|
|
|
|
const auto existingHash = QString::fromUtf8(QCryptographicHash::hash(existingFile.readAll(), QCryptographicHash::Algorithm::Sha256).toHex());
|
|
|
|
if (existingHash != hash) {
|
|
|
|
areFilesDifferent = true;
|
|
|
|
qCDebug(ASTRA_LOG) << id << "does not match locally, reason: hashes do not match for" << file;
|
|
|
|
break;
|
2024-07-28 11:17:30 -04:00
|
|
|
}
|
2024-07-30 20:41:35 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
const bool hasPreviousUpload = !previousData.has_value();
|
|
|
|
const bool isGameClosing = !initialSync;
|
2024-07-28 11:17:30 -04:00
|
|
|
|
2024-07-30 20:41:35 -04:00
|
|
|
// We want to upload if the files are truly different, or there is no existing data on the server.
|
|
|
|
const bool needsUpload = (areFilesDifferent && isGameClosing) || hasPreviousUpload;
|
|
|
|
|
|
|
|
// We want to download if the files are different.
|
|
|
|
const bool needsDownload = areFilesDifferent;
|
|
|
|
|
|
|
|
if (needsUpload) {
|
|
|
|
// if we didn't upload character data yet, upload it now
|
|
|
|
co_await uploadCharacterData(dir.absoluteFilePath(), id);
|
|
|
|
} else if (needsDownload) {
|
2024-07-28 11:17:30 -04:00
|
|
|
co_await downloadCharacterData(dir.absoluteFilePath(), id, previousData->mxcUri);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
co_return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
QCoro::Task<void> CharacterSync::uploadCharacterData(const QDir &dir, const QString &id)
|
|
|
|
{
|
|
|
|
qCDebug(ASTRA_LOG) << "Uploading" << dir << id;
|
2024-07-30 20:41:35 -04:00
|
|
|
const QTemporaryDir tempDir;
|
2024-07-28 11:17:30 -04:00
|
|
|
|
2024-07-30 20:41:35 -04:00
|
|
|
const auto tempZipPath = tempDir.filePath(QStringLiteral("%1.zip").arg(id));
|
2024-07-28 11:17:30 -04:00
|
|
|
|
2024-07-30 20:41:35 -04:00
|
|
|
const auto zip = new KZip(tempZipPath);
|
2024-07-28 11:17:30 -04:00
|
|
|
zip->setCompression(KZip::DeflateCompression);
|
|
|
|
zip->open(QIODevice::WriteOnly);
|
|
|
|
|
2024-07-30 20:41:35 -04:00
|
|
|
QFile gearsetFile(dir.filePath(gearsetFilename));
|
2024-07-28 11:17:30 -04:00
|
|
|
gearsetFile.open(QFile::ReadOnly);
|
|
|
|
|
2024-07-30 20:41:35 -04:00
|
|
|
const auto data = gearsetFile.readAll();
|
|
|
|
|
|
|
|
zip->writeFile(gearsetFilename, data);
|
2024-07-28 11:17:30 -04:00
|
|
|
zip->close();
|
2024-11-09 15:06:33 -05:00
|
|
|
delete zip;
|
2024-07-28 11:17:30 -04:00
|
|
|
|
2024-07-30 20:41:35 -04:00
|
|
|
QMap<QString, QString> fileHashes;
|
|
|
|
fileHashes[gearsetFilename] = QString::fromUtf8(QCryptographicHash::hash(data, QCryptographicHash::Algorithm::Sha256).toHex());
|
|
|
|
|
|
|
|
co_await launcher.syncManager()->uploadCharacterArchive(id, tempZipPath, fileHashes);
|
2024-07-28 11:17:30 -04:00
|
|
|
// TODO: error handling
|
|
|
|
|
|
|
|
co_return;
|
|
|
|
}
|
|
|
|
|
|
|
|
QCoro::Task<void> CharacterSync::downloadCharacterData(const QDir &dir, const QString &id, const QString &contentUri)
|
|
|
|
{
|
2024-07-30 20:41:35 -04:00
|
|
|
const QTemporaryDir tempDir;
|
2024-07-28 11:17:30 -04:00
|
|
|
|
2024-07-30 20:41:35 -04:00
|
|
|
const auto tempZipPath = tempDir.filePath(QStringLiteral("%1.zip").arg(id));
|
2024-07-28 11:17:30 -04:00
|
|
|
|
2024-07-30 20:41:35 -04:00
|
|
|
co_await launcher.syncManager()->downloadCharacterArchive(contentUri, tempZipPath);
|
2024-07-28 11:17:30 -04:00
|
|
|
|
2024-07-30 20:41:35 -04:00
|
|
|
auto zip = new KZip(tempZipPath);
|
2024-07-28 11:17:30 -04:00
|
|
|
zip->setCompression(KZip::DeflateCompression);
|
|
|
|
zip->open(QIODevice::ReadOnly);
|
|
|
|
|
|
|
|
qCDebug(ASTRA_LOG) << "contents:" << zip->directory()->entries();
|
|
|
|
|
2024-07-30 20:41:35 -04:00
|
|
|
Q_UNUSED(zip->directory()->file(gearsetFilename)->copyTo(dir.absolutePath()))
|
2024-07-28 11:17:30 -04:00
|
|
|
|
|
|
|
qCDebug(ASTRA_LOG) << "Extracted character data!";
|
|
|
|
|
|
|
|
zip->close();
|
2024-11-09 15:06:33 -05:00
|
|
|
delete zip;
|
2024-07-28 11:17:30 -04:00
|
|
|
|
|
|
|
co_return;
|
|
|
|
}
|
|
|
|
|
2024-11-09 15:06:33 -05:00
|
|
|
#include "moc_charactersync.cpp"
|