//// AssetsBackupHandler.cpp// domain-server/src//// Created by Clement Brisset on 1/12/18.// Copyright 2018 High Fidelity, Inc.//// Distributed under the Apache License, Version 2.0.// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html// #include "AssetsBackupHandler.h" #include <QJsonDocument>#include <QDate>#include <QtCore/QLoggingCategory> #if !defined(__clang__) && defined(__GNUC__)#pragma GCC diagnostic push#pragma GCC diagnostic ignored "-Wsuggest-override"#endif #include <quazip/quazipfile.h>#include <quazip/quazipdir.h> #if !defined(__clang__) && defined(__GNUC__)#pragma GCC diagnostic pop#endif #include <AssetClient.h>#include <AssetRequest.h>#include <AssetUpload.h>#include <MappingRequest.h>#include <PathUtils.h> using namespace std; static const QString ASSETS_DIR { "/assets/" };static const QString MAPPINGS_FILE { "mappings.json" };static const QString ZIP_ASSETS_FOLDER { "files" };static const chrono::minutes MAX_REFRESH_TIME { 5 }; Q_DECLARE_LOGGING_CATEGORY(asset_backup)Q_LOGGING_CATEGORY(asset_backup, "hifi.asset-backup"); AssetsBackupHandler::AssetsBackupHandler(const QString& backupDirectory, bool assetServerEnabled) : _assetsDirectory(backupDirectory + ASSETS_DIR), _assetServerEnabled(assetServerEnabled){ // Make sure the asset directory exists. QDir(_assetsDirectory).mkpath("."); refreshAssetsOnDisk(); setupRefreshTimer();} void AssetsBackupHandler::setupRefreshTimer() { _mappingsRefreshTimer.setTimerType(Qt::CoarseTimer); _mappingsRefreshTimer.setSingleShot(true); QObject::connect(&_mappingsRefreshTimer, &QTimer::timeout, this, &AssetsBackupHandler::refreshMappings); auto nodeList = DependencyManager::get<LimitedNodeList>(); QObject::connect(nodeList.data(), &LimitedNodeList::nodeActivated, this, [this](SharedNodePointer node) { if (node->getType() == NodeType::AssetServer) { assert(_assetServerEnabled); // run almost immediately for the first time. _mappingsRefreshTimer.start(2); } }); QObject::connect(nodeList.data(), &LimitedNodeList::nodeKilled, this, [this](SharedNodePointer node) { if (node->getType() == NodeType::AssetServer) { _mappingsRefreshTimer.stop(); } });} void AssetsBackupHandler::refreshAssetsOnDisk() { QDir assetsDir { _assetsDirectory }; auto assetNames = assetsDir.entryList(QDir::Files); // store all valid hashes copy_if(begin(assetNames), end(assetNames), inserter(_assetsOnDisk, begin(_assetsOnDisk)), AssetUtils::isValidHash); } void AssetsBackupHandler::refreshAssetsInBackups() { _assetsInBackups.clear(); for (const auto& backup : _backups) { for (const auto& mapping : backup.mappings) { _assetsInBackups.insert(mapping.second); } }} void AssetsBackupHandler::checkForMissingAssets() { vector<AssetUtils::AssetHash> missingAssets; set_difference(begin(_assetsInBackups), end(_assetsInBackups), begin(_assetsOnDisk), end(_assetsOnDisk), back_inserter(missingAssets)); if (missingAssets.size() > 0) { qCWarning(asset_backup) << "Found" << missingAssets.size() << "backup assets missing from disk."; }} void AssetsBackupHandler::checkForAssetsToDelete() { vector<AssetUtils::AssetHash> deprecatedAssets; set_difference(begin(_assetsOnDisk), end(_assetsOnDisk), begin(_assetsInBackups), end(_assetsInBackups), back_inserter(deprecatedAssets)); if (deprecatedAssets.size() > 0) { qCDebug(asset_backup) << "Found" << deprecatedAssets.size() << "backup assets to delete from disk."; const auto noCorruptedBackups = none_of(begin(_backups), end(_backups), [&](const AssetServerBackup& backup) { return backup.corruptedBackup; }); if (noCorruptedBackups) { for (const auto& hash : deprecatedAssets) { auto success = QFile::remove(_assetsDirectory + hash); if (success) { _assetsOnDisk.erase(hash); } else { qCWarning(asset_backup) << "Could not delete asset:" << hash; } } } else { qCWarning(asset_backup) << "Some backups did not load properly, aborting delete operation for safety."; } }} bool AssetsBackupHandler::isCorruptedBackup(const QString& backupName) { auto it = find_if(begin(_backups), end(_backups), [&](const AssetServerBackup& value) { return value.name == backupName; }); if (it == end(_backups)) { return false; } return it->corruptedBackup;} std::pair<bool, float> AssetsBackupHandler::isAvailable(const QString& backupName) { const auto it = find_if(begin(_backups), end(_backups), [&](const AssetServerBackup& backup) { return backup.name == backupName; }); if (it == end(_backups)) { return { true, 1.0f }; } int mappingsMissing = 0; for (const auto& mapping : it->mappings) { if (_assetsLeftToRequest.find(mapping.second) != end(_assetsLeftToRequest)) { ++mappingsMissing; } } if (mappingsMissing == 0) { return { true, 1.0f }; } float progress = (float)it->mappings.size(); progress -= (float)mappingsMissing; progress /= it->mappings.size(); return { false, progress };} std::pair<bool, float> AssetsBackupHandler::getRecoveryStatus() { if (_assetsLeftToUpload.empty() && _mappingsLeftToSet.empty() && _mappingsLeftToDelete.empty() && _mappingRequestsInFlight == 0) { return { false, 1.0f }; } float progress = (float)_numRestoreOperations; progress -= (float)_assetsLeftToUpload.size(); progress -= (float)_mappingRequestsInFlight; progress /= (float)_numRestoreOperations; return { true, progress };} void AssetsBackupHandler::loadBackup(const QString& backupName, QuaZip& zip) { Q_ASSERT(QThread::currentThread() == thread()); _backups.emplace_back(backupName, AssetUtils::Mappings(), false); auto& backup = _backups.back(); if (!zip.setCurrentFile(MAPPINGS_FILE)) { qCCritical(asset_backup) << "Failed to find" << MAPPINGS_FILE << "while loading backup"; qCCritical(asset_backup) << " Error:" << zip.getZipError(); backup.corruptedBackup = true; return; } QuaZipFile zipFile { &zip }; if (!zipFile.open(QFile::ReadOnly)) { qCCritical(asset_backup) << "Could not unzip backup file for load:" << MAPPINGS_FILE; qCCritical(asset_backup) << " Error:" << zip.getZipError(); backup.corruptedBackup = true; return; } QJsonParseError error; auto document = QJsonDocument::fromJson(zipFile.readAll(), &error); if (document.isNull() || !document.isObject()) { qCCritical(asset_backup) << "Could not parse backup file to JSON object for load:" << MAPPINGS_FILE; qCCritical(asset_backup) << " Error:" << error.errorString(); backup.corruptedBackup = true; return; } auto jsonObject = document.object(); for (auto it = begin(jsonObject); it != end(jsonObject); ++it) { const auto& assetPath = it.key(); const auto& assetHash = it.value().toString(); if (!AssetUtils::isValidHash(assetHash)) { qCCritical(asset_backup) << "Corrupted mapping in loading backup file" << backupName << ":" << it.key(); backup.corruptedBackup = true; continue; } backup.mappings[assetPath] = assetHash; _assetsInBackups.insert(assetHash); }} void AssetsBackupHandler::loadingComplete() { checkForMissingAssets(); checkForAssetsToDelete();} void AssetsBackupHandler::createBackup(const QString& backupName, QuaZip& zip) { Q_ASSERT(QThread::currentThread() == thread()); if (operationInProgress()) { qCWarning(asset_backup) << "There is already an operation in progress."; return; } if (_assetServerEnabled && _lastMappingsRefresh.time_since_epoch().count() == 0) { qCWarning(asset_backup) << "Current mappings not yet loaded."; _backups.emplace_back(backupName, AssetUtils::Mappings(), true); return; } if (_assetServerEnabled && (p_high_resolution_clock::now() - _lastMappingsRefresh) > MAX_REFRESH_TIME) { qCWarning(asset_backup) << "Backing up asset mappings that might be stale."; } AssetUtils::Mappings mappings; QJsonObject jsonObject; for (const auto& mapping : _currentMappings) { mappings[mapping.first] = mapping.second; _assetsInBackups.insert(mapping.second); jsonObject.insert(mapping.first, mapping.second); } QJsonDocument document(jsonObject); QuaZipFile zipFile { &zip }; if (!zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo(MAPPINGS_FILE))) { qCDebug(asset_backup) << "Could not open zip file:" << zipFile.getZipError(); return; } zipFile.write(document.toJson()); zipFile.close(); if (zipFile.getZipError() != UNZ_OK) { qCDebug(asset_backup) << "Could not close zip file: " << zipFile.getZipError(); return; } _backups.emplace_back(backupName, mappings, false);} std::pair<bool, QString> AssetsBackupHandler::recoverBackup(const QString& backupName, QuaZip& zip, const QString& username, const QString& sourceFilename) { Q_ASSERT(QThread::currentThread() == thread()); if (operationInProgress()) { QString errorStr ("There is already a backup/restore in progress. Please wait."); qWarning() << errorStr; return { false, errorStr }; } if (_lastMappingsRefresh.time_since_epoch().count() == 0) { QString errorStr ("Current mappings not yet loaded. Please wait."); qWarning() << errorStr; return { false, errorStr }; } if ((p_high_resolution_clock::now() - _lastMappingsRefresh) > MAX_REFRESH_TIME) { qCWarning(asset_backup) << "Recovering while current asset mappings might be stale."; } auto it = find_if(begin(_backups), end(_backups), [&](const AssetServerBackup& backup) { return backup.name == backupName; }); if (it == end(_backups)) { loadBackup(backupName, zip); auto emplaced_backup = find_if(begin(_backups), end(_backups), [&](const AssetServerBackup& backup) { return backup.name == backupName; }); if(emplaced_backup->corruptedBackup) { QString errorStr ("Current mappings file is corrupted."); qWarning() << errorStr; return { false, errorStr }; } QuaZipDir zipDir { &zip, ZIP_ASSETS_FOLDER }; auto assetNames = zipDir.entryList(QDir::Files); for (const auto& asset : assetNames) { if (AssetUtils::isValidHash(asset)) { if (!zip.setCurrentFile(zipDir.filePath(asset))) { qCCritical(asset_backup) << "Failed to find" << asset << "while recovering backup"; qCCritical(asset_backup) << " Error:" << zip.getZipError(); continue; } QuaZipFile zipFile { &zip }; if (!zipFile.open(QFile::ReadOnly)) { qCCritical(asset_backup) << "Could not unzip asset file:" << asset; qCCritical(asset_backup) << " Error:" << zip.getZipError(); continue; } writeAssetFile(asset, zipFile.readAll()); } } // iterator is end() and has been invalidated in the `loadBackup` call // grab the new iterator it = find_if(begin(_backups), end(_backups), [&](const AssetServerBackup& backup) { return backup.name == backupName; }); if (it == end(_backups)) { QString errorStr ("Failed to recover backup: " + backupName); qWarning() << errorStr; return { false, errorStr }; } } const auto& newMappings = it->mappings; computeServerStateDifference(_currentMappings, newMappings); restoreAllAssets(); return { true, QString() };} void AssetsBackupHandler::deleteBackup(const QString& backupName) { Q_ASSERT(QThread::currentThread() == thread()); if (operationInProgress()) { qCWarning(asset_backup) << "There is a backup/restore in progress."; return; } const auto it = remove_if(begin(_backups), end(_backups), [&](const AssetServerBackup& backup) { return backup.name == backupName; }); if (it == end(_backups)) { qCDebug(asset_backup) << "Could not find backup" << backupName << "to delete."; return; } _backups.erase(it, end(_backups)); refreshAssetsInBackups(); checkForAssetsToDelete(); qDebug() << "Deleted asset backup:" << backupName;} void AssetsBackupHandler::consolidateBackup(const QString& backupName, QuaZip& zip) { Q_ASSERT(QThread::currentThread() == thread()); if (operationInProgress()) { qCWarning(asset_backup) << "There is a backup/restore in progress."; return; } const auto it = find_if(begin(_backups), end(_backups), [&](const AssetServerBackup& backup) { return backup.name == backupName; }); if (it == end(_backups)) { qCDebug(asset_backup) << "Could not find backup" << backupName << "to consolidate."; return; } for (const auto& mapping : it->mappings) { const auto& hash = mapping.second; QDir assetsDir { _assetsDirectory }; QFile file { assetsDir.filePath(hash) }; if (!file.open(QFile::ReadOnly)) { qCCritical(asset_backup) << "Could not open asset file" << file.fileName(); continue; } QuaZipFile zipFile { &zip }; if (!zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo(ZIP_ASSETS_FOLDER + "/" + hash))) { qCDebug(asset_backup) << "Could not open zip file:" << zipFile.getZipError(); continue; } zipFile.write(file.readAll()); zipFile.close(); if (zipFile.getZipError() != UNZ_OK) { qCDebug(asset_backup) << "Could not close zip file: " << zipFile.getZipError(); continue; } } } void AssetsBackupHandler::refreshMappings() { auto assetClient = DependencyManager::get<AssetClient>(); auto request = assetClient->createGetAllMappingsRequest(); QObject::connect(request, &GetAllMappingsRequest::finished, this, [this](GetAllMappingsRequest* request) { if (request->getError() == MappingRequest::NoError) { const auto& mappings = request->getMappings(); // Clear existing mappings _currentMappings.clear(); // Set new mapping, but ignore baked assets for (const auto& mapping : mappings) { if (!mapping.first.startsWith(AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER)) { _currentMappings.insert({ mapping.first, mapping.second.hash }); } } _lastMappingsRefresh = p_high_resolution_clock::now(); downloadMissingFiles(_currentMappings); } else { qCCritical(asset_backup) << "Could not refresh asset server mappings."; qCCritical(asset_backup) << " Error:" << request->getErrorString(); } request->deleteLater(); // Launch next mappings request static constexpr int MAPPINGS_REFRESH_INTERVAL = 30 * 1000; _mappingsRefreshTimer.start(MAPPINGS_REFRESH_INTERVAL); }); request->start();} void AssetsBackupHandler::downloadMissingFiles(const AssetUtils::Mappings& mappings) { auto wasEmpty = _assetsLeftToRequest.empty(); for (const auto& mapping : mappings) { const auto& hash = mapping.second; if (_assetsOnDisk.find(hash) == end(_assetsOnDisk)) { _assetsLeftToRequest.insert(hash); } } // If we were empty, that means no download chain was already going, start one. if (wasEmpty) { downloadNextMissingFile(); }} void AssetsBackupHandler::downloadNextMissingFile() { if (_assetsLeftToRequest.empty()) { return; } auto hash = *begin(_assetsLeftToRequest); auto assetClient = DependencyManager::get<AssetClient>(); auto assetRequest = assetClient->createRequest(hash); QObject::connect(assetRequest, &AssetRequest::finished, this, [this](AssetRequest* request) { if (request->getError() == AssetRequest::NoError) { qCDebug(asset_backup) << "Backing up asset" << request->getHash(); bool success = writeAssetFile(request->getHash(), request->getData()); if (!success) { qCCritical(asset_backup) << "Failed to write asset file" << request->getHash(); } } else { qCCritical(asset_backup) << "Failed to backup asset" << request->getHash(); } _assetsLeftToRequest.erase(request->getHash()); downloadNextMissingFile(); request->deleteLater(); }); assetRequest->start();Lines 1–500 of 630. Load more
// // AssetsBackupHandler.cpp // domain-server/src // // Created by Clement Brisset on 1/12/18. // Copyright 2018 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // #include "AssetsBackupHandler.h" #include <QJsonDocument> #include <QDate> #include <QtCore/QLoggingCategory> #if !defined(__clang__) && defined(__GNUC__) #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wsuggest-override" #endif #include <quazip/quazipfile.h> #include <quazip/quazipdir.h> #if !defined(__clang__) && defined(__GNUC__) #pragma GCC diagnostic pop #endif #include <AssetClient.h> #include <AssetRequest.h> #include <AssetUpload.h> #include <MappingRequest.h> #include <PathUtils.h> using namespace std; static const QString ASSETS_DIR { "/assets/" }; static const QString MAPPINGS_FILE { "mappings.json" }; static const QString ZIP_ASSETS_FOLDER { "files" }; static const chrono::minutes MAX_REFRESH_TIME { 5 }; Q_DECLARE_LOGGING_CATEGORY(asset_backup) Q_LOGGING_CATEGORY(asset_backup, "hifi.asset-backup"); AssetsBackupHandler::AssetsBackupHandler(const QString& backupDirectory, bool assetServerEnabled) : _assetsDirectory(backupDirectory + ASSETS_DIR), _assetServerEnabled(assetServerEnabled) { // Make sure the asset directory exists. QDir(_assetsDirectory).mkpath("."); refreshAssetsOnDisk(); setupRefreshTimer(); } void AssetsBackupHandler::setupRefreshTimer() { _mappingsRefreshTimer.setTimerType(Qt::CoarseTimer); _mappingsRefreshTimer.setSingleShot(true); QObject::connect(&_mappingsRefreshTimer, &QTimer::timeout, this, &AssetsBackupHandler::refreshMappings); auto nodeList = DependencyManager::get<LimitedNodeList>(); QObject::connect(nodeList.data(), &LimitedNodeList::nodeActivated, this, [this](SharedNodePointer node) { if (node->getType() == NodeType::AssetServer) { assert(_assetServerEnabled); // run almost immediately for the first time. _mappingsRefreshTimer.start(2); } }); QObject::connect(nodeList.data(), &LimitedNodeList::nodeKilled, this, [this](SharedNodePointer node) { if (node->getType() == NodeType::AssetServer) { _mappingsRefreshTimer.stop(); } }); } void AssetsBackupHandler::refreshAssetsOnDisk() { QDir assetsDir { _assetsDirectory }; auto assetNames = assetsDir.entryList(QDir::Files); // store all valid hashes copy_if(begin(assetNames), end(assetNames), inserter(_assetsOnDisk, begin(_assetsOnDisk)), AssetUtils::isValidHash); } void AssetsBackupHandler::refreshAssetsInBackups() { _assetsInBackups.clear(); for (const auto& backup : _backups) { for (const auto& mapping : backup.mappings) { _assetsInBackups.insert(mapping.second); } } } void AssetsBackupHandler::checkForMissingAssets() { vector<AssetUtils::AssetHash> missingAssets; set_difference(begin(_assetsInBackups), end(_assetsInBackups), begin(_assetsOnDisk), end(_assetsOnDisk), back_inserter(missingAssets)); if (missingAssets.size() > 0) { qCWarning(asset_backup) << "Found" << missingAssets.size() << "backup assets missing from disk."; } } void AssetsBackupHandler::checkForAssetsToDelete() { vector<AssetUtils::AssetHash> deprecatedAssets; set_difference(begin(_assetsOnDisk), end(_assetsOnDisk), begin(_assetsInBackups), end(_assetsInBackups), back_inserter(deprecatedAssets)); if (deprecatedAssets.size() > 0) { qCDebug(asset_backup) << "Found" << deprecatedAssets.size() << "backup assets to delete from disk."; const auto noCorruptedBackups = none_of(begin(_backups), end(_backups), [&](const AssetServerBackup& backup) { return backup.corruptedBackup; }); if (noCorruptedBackups) { for (const auto& hash : deprecatedAssets) { auto success = QFile::remove(_assetsDirectory + hash); if (success) { _assetsOnDisk.erase(hash); } else { qCWarning(asset_backup) << "Could not delete asset:" << hash; } } } else { qCWarning(asset_backup) << "Some backups did not load properly, aborting delete operation for safety."; } } } bool AssetsBackupHandler::isCorruptedBackup(const QString& backupName) { auto it = find_if(begin(_backups), end(_backups), [&](const AssetServerBackup& value) { return value.name == backupName; }); if (it == end(_backups)) { return false; } return it->corruptedBackup; } std::pair<bool, float> AssetsBackupHandler::isAvailable(const QString& backupName) { const auto it = find_if(begin(_backups), end(_backups), [&](const AssetServerBackup& backup) { return backup.name == backupName; }); if (it == end(_backups)) { return { true, 1.0f }; } int mappingsMissing = 0; for (const auto& mapping : it->mappings) { if (_assetsLeftToRequest.find(mapping.second) != end(_assetsLeftToRequest)) { ++mappingsMissing; } } if (mappingsMissing == 0) { return { true, 1.0f }; } float progress = (float)it->mappings.size(); progress -= (float)mappingsMissing; progress /= it->mappings.size(); return { false, progress }; } std::pair<bool, float> AssetsBackupHandler::getRecoveryStatus() { if (_assetsLeftToUpload.empty() && _mappingsLeftToSet.empty() && _mappingsLeftToDelete.empty() && _mappingRequestsInFlight == 0) { return { false, 1.0f }; } float progress = (float)_numRestoreOperations; progress -= (float)_assetsLeftToUpload.size(); progress -= (float)_mappingRequestsInFlight; progress /= (float)_numRestoreOperations; return { true, progress }; } void AssetsBackupHandler::loadBackup(const QString& backupName, QuaZip& zip) { Q_ASSERT(QThread::currentThread() == thread()); _backups.emplace_back(backupName, AssetUtils::Mappings(), false); auto& backup = _backups.back(); if (!zip.setCurrentFile(MAPPINGS_FILE)) { qCCritical(asset_backup) << "Failed to find" << MAPPINGS_FILE << "while loading backup"; qCCritical(asset_backup) << " Error:" << zip.getZipError(); backup.corruptedBackup = true; return; } QuaZipFile zipFile { &zip }; if (!zipFile.open(QFile::ReadOnly)) { qCCritical(asset_backup) << "Could not unzip backup file for load:" << MAPPINGS_FILE; qCCritical(asset_backup) << " Error:" << zip.getZipError(); backup.corruptedBackup = true; return; } QJsonParseError error; auto document = QJsonDocument::fromJson(zipFile.readAll(), &error); if (document.isNull() || !document.isObject()) { qCCritical(asset_backup) << "Could not parse backup file to JSON object for load:" << MAPPINGS_FILE; qCCritical(asset_backup) << " Error:" << error.errorString(); backup.corruptedBackup = true; return; } auto jsonObject = document.object(); for (auto it = begin(jsonObject); it != end(jsonObject); ++it) { const auto& assetPath = it.key(); const auto& assetHash = it.value().toString(); if (!AssetUtils::isValidHash(assetHash)) { qCCritical(asset_backup) << "Corrupted mapping in loading backup file" << backupName << ":" << it.key(); backup.corruptedBackup = true; continue; } backup.mappings[assetPath] = assetHash; _assetsInBackups.insert(assetHash); } } void AssetsBackupHandler::loadingComplete() { checkForMissingAssets(); checkForAssetsToDelete(); } void AssetsBackupHandler::createBackup(const QString& backupName, QuaZip& zip) { Q_ASSERT(QThread::currentThread() == thread()); if (operationInProgress()) { qCWarning(asset_backup) << "There is already an operation in progress."; return; } if (_assetServerEnabled && _lastMappingsRefresh.time_since_epoch().count() == 0) { qCWarning(asset_backup) << "Current mappings not yet loaded."; _backups.emplace_back(backupName, AssetUtils::Mappings(), true); return; } if (_assetServerEnabled && (p_high_resolution_clock::now() - _lastMappingsRefresh) > MAX_REFRESH_TIME) { qCWarning(asset_backup) << "Backing up asset mappings that might be stale."; } AssetUtils::Mappings mappings; QJsonObject jsonObject; for (const auto& mapping : _currentMappings) { mappings[mapping.first] = mapping.second; _assetsInBackups.insert(mapping.second); jsonObject.insert(mapping.first, mapping.second); } QJsonDocument document(jsonObject); QuaZipFile zipFile { &zip }; if (!zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo(MAPPINGS_FILE))) { qCDebug(asset_backup) << "Could not open zip file:" << zipFile.getZipError(); return; } zipFile.write(document.toJson()); zipFile.close(); if (zipFile.getZipError() != UNZ_OK) { qCDebug(asset_backup) << "Could not close zip file: " << zipFile.getZipError(); return; } _backups.emplace_back(backupName, mappings, false); } std::pair<bool, QString> AssetsBackupHandler::recoverBackup(const QString& backupName, QuaZip& zip, const QString& username, const QString& sourceFilename) { Q_ASSERT(QThread::currentThread() == thread()); if (operationInProgress()) { QString errorStr ("There is already a backup/restore in progress. Please wait."); qWarning() << errorStr; return { false, errorStr }; } if (_lastMappingsRefresh.time_since_epoch().count() == 0) { QString errorStr ("Current mappings not yet loaded. Please wait."); qWarning() << errorStr; return { false, errorStr }; } if ((p_high_resolution_clock::now() - _lastMappingsRefresh) > MAX_REFRESH_TIME) { qCWarning(asset_backup) << "Recovering while current asset mappings might be stale."; } auto it = find_if(begin(_backups), end(_backups), [&](const AssetServerBackup& backup) { return backup.name == backupName; }); if (it == end(_backups)) { loadBackup(backupName, zip); auto emplaced_backup = find_if(begin(_backups), end(_backups), [&](const AssetServerBackup& backup) { return backup.name == backupName; }); if(emplaced_backup->corruptedBackup) { QString errorStr ("Current mappings file is corrupted."); qWarning() << errorStr; return { false, errorStr }; } QuaZipDir zipDir { &zip, ZIP_ASSETS_FOLDER }; auto assetNames = zipDir.entryList(QDir::Files); for (const auto& asset : assetNames) { if (AssetUtils::isValidHash(asset)) { if (!zip.setCurrentFile(zipDir.filePath(asset))) { qCCritical(asset_backup) << "Failed to find" << asset << "while recovering backup"; qCCritical(asset_backup) << " Error:" << zip.getZipError(); continue; } QuaZipFile zipFile { &zip }; if (!zipFile.open(QFile::ReadOnly)) { qCCritical(asset_backup) << "Could not unzip asset file:" << asset; qCCritical(asset_backup) << " Error:" << zip.getZipError(); continue; } writeAssetFile(asset, zipFile.readAll()); } } // iterator is end() and has been invalidated in the `loadBackup` call // grab the new iterator it = find_if(begin(_backups), end(_backups), [&](const AssetServerBackup& backup) { return backup.name == backupName; }); if (it == end(_backups)) { QString errorStr ("Failed to recover backup: " + backupName); qWarning() << errorStr; return { false, errorStr }; } } const auto& newMappings = it->mappings; computeServerStateDifference(_currentMappings, newMappings); restoreAllAssets(); return { true, QString() }; } void AssetsBackupHandler::deleteBackup(const QString& backupName) { Q_ASSERT(QThread::currentThread() == thread()); if (operationInProgress()) { qCWarning(asset_backup) << "There is a backup/restore in progress."; return; } const auto it = remove_if(begin(_backups), end(_backups), [&](const AssetServerBackup& backup) { return backup.name == backupName; }); if (it == end(_backups)) { qCDebug(asset_backup) << "Could not find backup" << backupName << "to delete."; return; } _backups.erase(it, end(_backups)); refreshAssetsInBackups(); checkForAssetsToDelete(); qDebug() << "Deleted asset backup:" << backupName; } void AssetsBackupHandler::consolidateBackup(const QString& backupName, QuaZip& zip) { Q_ASSERT(QThread::currentThread() == thread()); if (operationInProgress()) { qCWarning(asset_backup) << "There is a backup/restore in progress."; return; } const auto it = find_if(begin(_backups), end(_backups), [&](const AssetServerBackup& backup) { return backup.name == backupName; }); if (it == end(_backups)) { qCDebug(asset_backup) << "Could not find backup" << backupName << "to consolidate."; return; } for (const auto& mapping : it->mappings) { const auto& hash = mapping.second; QDir assetsDir { _assetsDirectory }; QFile file { assetsDir.filePath(hash) }; if (!file.open(QFile::ReadOnly)) { qCCritical(asset_backup) << "Could not open asset file" << file.fileName(); continue; } QuaZipFile zipFile { &zip }; if (!zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo(ZIP_ASSETS_FOLDER + "/" + hash))) { qCDebug(asset_backup) << "Could not open zip file:" << zipFile.getZipError(); continue; } zipFile.write(file.readAll()); zipFile.close(); if (zipFile.getZipError() != UNZ_OK) { qCDebug(asset_backup) << "Could not close zip file: " << zipFile.getZipError(); continue; } } } void AssetsBackupHandler::refreshMappings() { auto assetClient = DependencyManager::get<AssetClient>(); auto request = assetClient->createGetAllMappingsRequest(); QObject::connect(request, &GetAllMappingsRequest::finished, this, [this](GetAllMappingsRequest* request) { if (request->getError() == MappingRequest::NoError) { const auto& mappings = request->getMappings(); // Clear existing mappings _currentMappings.clear(); // Set new mapping, but ignore baked assets for (const auto& mapping : mappings) { if (!mapping.first.startsWith(AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER)) { _currentMappings.insert({ mapping.first, mapping.second.hash }); } } _lastMappingsRefresh = p_high_resolution_clock::now(); downloadMissingFiles(_currentMappings); } else { qCCritical(asset_backup) << "Could not refresh asset server mappings."; qCCritical(asset_backup) << " Error:" << request->getErrorString(); } request->deleteLater(); // Launch next mappings request static constexpr int MAPPINGS_REFRESH_INTERVAL = 30 * 1000; _mappingsRefreshTimer.start(MAPPINGS_REFRESH_INTERVAL); }); request->start(); } void AssetsBackupHandler::downloadMissingFiles(const AssetUtils::Mappings& mappings) { auto wasEmpty = _assetsLeftToRequest.empty(); for (const auto& mapping : mappings) { const auto& hash = mapping.second; if (_assetsOnDisk.find(hash) == end(_assetsOnDisk)) { _assetsLeftToRequest.insert(hash); } } // If we were empty, that means no download chain was already going, start one. if (wasEmpty) { downloadNextMissingFile(); } } void AssetsBackupHandler::downloadNextMissingFile() { if (_assetsLeftToRequest.empty()) { return; } auto hash = *begin(_assetsLeftToRequest); auto assetClient = DependencyManager::get<AssetClient>(); auto assetRequest = assetClient->createRequest(hash); QObject::connect(assetRequest, &AssetRequest::finished, this, [this](AssetRequest* request) { if (request->getError() == AssetRequest::NoError) { qCDebug(asset_backup) << "Backing up asset" << request->getHash(); bool success = writeAssetFile(request->getHash(), request->getData()); if (!success) { qCCritical(asset_backup) << "Failed to write asset file" << request->getHash(); } } else { qCCritical(asset_backup) << "Failed to backup asset" << request->getHash(); } _assetsLeftToRequest.erase(request->getHash()); downloadNextMissingFile(); request->deleteLater(); }); assetRequest->start();