Merge remote-tracking branch 'upstream/develop' into updater_tests
Conflicts: mmc_updater/src/tests/CMakeLists.txt
This commit is contained in:
commit
47bf7fff27
@ -300,8 +300,6 @@ logic/net/NetJob.h
|
||||
logic/net/NetJob.cpp
|
||||
logic/net/HttpMetaCache.h
|
||||
logic/net/HttpMetaCache.cpp
|
||||
logic/net/S3ListBucket.h
|
||||
logic/net/S3ListBucket.cpp
|
||||
logic/net/PasteUpload.h
|
||||
logic/net/PasteUpload.cpp
|
||||
logic/net/URLConstants.h
|
||||
@ -482,10 +480,10 @@ QT5_ADD_RESOURCES(GENERATED_QRC ${CMAKE_CURRENT_BINARY_DIR}/generated.qrc)
|
||||
QT5_ADD_RESOURCES(GRAPHICS_QRC graphics.qrc)
|
||||
|
||||
# Add common library
|
||||
ADD_LIBRARY(MultiMC_common STATIC ${MULTIMC_SOURCES} ${MULTIMC_UI} ${GENERATED_QRC} ${GRAPHICS_QRC} ${MULTIMC_RCS})
|
||||
ADD_LIBRARY(MultiMC_common STATIC ${MULTIMC_SOURCES} ${MULTIMC_UI} ${GENERATED_QRC} ${GRAPHICS_QRC})
|
||||
|
||||
# Add executable
|
||||
ADD_EXECUTABLE(MultiMC MACOSX_BUNDLE WIN32 main.cpp)
|
||||
ADD_EXECUTABLE(MultiMC MACOSX_BUNDLE WIN32 main.cpp ${MULTIMC_RCS})
|
||||
|
||||
# Link
|
||||
TARGET_LINK_LIBRARIES(MultiMC MultiMC_common)
|
||||
|
@ -58,10 +58,17 @@ ConsoleWindow::~ConsoleWindow()
|
||||
void ConsoleWindow::writeColor(QString text, const char *color)
|
||||
{
|
||||
// append a paragraph
|
||||
if (color != nullptr)
|
||||
ui->text->appendHtml(QString("<font color=\"%1\">%2</font>").arg(color).arg(text));
|
||||
else
|
||||
ui->text->appendPlainText(text);
|
||||
QString newtext;
|
||||
newtext += "<span style=\"";
|
||||
{
|
||||
if(color)
|
||||
newtext += QString("color:") + color + ";";
|
||||
newtext += "font-family: monospace;";
|
||||
}
|
||||
newtext += "\">";
|
||||
newtext += text.toHtmlEscaped();
|
||||
newtext += "</span>";
|
||||
ui->text->appendHtml(newtext);
|
||||
}
|
||||
|
||||
void ConsoleWindow::write(QString data, MessageLevel::Enum mode)
|
||||
|
@ -17,11 +17,6 @@
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QPlainTextEdit" name="text">
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>10</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
<property name="undoRedoEnabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
|
@ -20,7 +20,6 @@
|
||||
|
||||
#include "MainWindow.h"
|
||||
#include "ui_MainWindow.h"
|
||||
#include "keyring.h"
|
||||
|
||||
#include <QMenu>
|
||||
#include <QMessageBox>
|
||||
@ -177,14 +176,15 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi
|
||||
statusBar()->addPermanentWidget(m_statusRight, 0);
|
||||
|
||||
// Add "manage accounts" button, right align
|
||||
QWidget* spacer = new QWidget();
|
||||
QWidget *spacer = new QWidget();
|
||||
spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
||||
ui->mainToolBar->addWidget(spacer);
|
||||
|
||||
accountMenu = new QMenu(this);
|
||||
manageAccountsAction = new QAction(tr("Manage Accounts"), this);
|
||||
manageAccountsAction->setCheckable(false);
|
||||
connect(manageAccountsAction, SIGNAL(triggered(bool)), this, SLOT(on_actionManageAccounts_triggered()));
|
||||
connect(manageAccountsAction, SIGNAL(triggered(bool)), this,
|
||||
SLOT(on_actionManageAccounts_triggered()));
|
||||
|
||||
repopulateAccountsMenu();
|
||||
|
||||
@ -193,7 +193,8 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi
|
||||
accountMenuButton->setMenu(accountMenu);
|
||||
accountMenuButton->setPopupMode(QToolButton::InstantPopup);
|
||||
accountMenuButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
|
||||
accountMenuButton->setIcon(QPixmap(":/icons/toolbar/noaccount").scaled(48, 48, Qt::KeepAspectRatio));
|
||||
accountMenuButton->setIcon(
|
||||
QPixmap(":/icons/toolbar/noaccount").scaled(48, 48, Qt::KeepAspectRatio));
|
||||
|
||||
QWidgetAction *accountMenuButtonAction = new QWidgetAction(this);
|
||||
accountMenuButtonAction->setDefaultWidget(accountMenuButton);
|
||||
@ -201,26 +202,28 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi
|
||||
ui->mainToolBar->addAction(accountMenuButtonAction);
|
||||
|
||||
// Update the menu when the active account changes.
|
||||
// Shouldn't have to use lambdas here like this, but if I don't, the compiler throws a fit. Template hell sucks...
|
||||
connect(MMC->accounts().get(), &MojangAccountList::activeAccountChanged, [this] { activeAccountChanged(); });
|
||||
connect(MMC->accounts().get(), &MojangAccountList::listChanged, [this] { repopulateAccountsMenu(); });
|
||||
// Shouldn't have to use lambdas here like this, but if I don't, the compiler throws a fit.
|
||||
// Template hell sucks...
|
||||
connect(MMC->accounts().get(), &MojangAccountList::activeAccountChanged, [this]
|
||||
{ activeAccountChanged(); });
|
||||
connect(MMC->accounts().get(), &MojangAccountList::listChanged, [this]
|
||||
{ repopulateAccountsMenu(); });
|
||||
|
||||
std::shared_ptr<MojangAccountList> accounts = MMC->accounts();
|
||||
|
||||
// TODO: Nicer way to iterate?
|
||||
for(int i = 0; i < accounts->count(); i++)
|
||||
for (int i = 0; i < accounts->count(); i++)
|
||||
{
|
||||
MojangAccountPtr account = accounts->at(i);
|
||||
if(account != nullptr)
|
||||
if (account != nullptr)
|
||||
{
|
||||
auto job = new NetJob("Startup player skins: " + account->username());
|
||||
|
||||
for(AccountProfile profile : account->profiles())
|
||||
for (AccountProfile profile : account->profiles())
|
||||
{
|
||||
auto meta = MMC->metacache()->resolveEntry("skins", profile.name + ".png");
|
||||
auto action = CacheDownload::make(
|
||||
QUrl("http://" + URLConstants::SKINS_BASE + profile.name + ".png"),
|
||||
meta);
|
||||
QUrl("http://" + URLConstants::SKINS_BASE + profile.name + ".png"), meta);
|
||||
job->addNetAction(action);
|
||||
meta->stale = true;
|
||||
}
|
||||
@ -245,9 +248,10 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi
|
||||
|
||||
// set up the updater object.
|
||||
auto updater = MMC->updateChecker();
|
||||
QObject::connect(updater.get(), &UpdateChecker::updateAvailable, this, &MainWindow::updateAvailable);
|
||||
QObject::connect(updater.get(), &UpdateChecker::updateAvailable, this,
|
||||
&MainWindow::updateAvailable);
|
||||
// if automatic update checks are allowed, start one.
|
||||
if(MMC->settings()->get("AutoUpdate").toBool())
|
||||
if (MMC->settings()->get("AutoUpdate").toBool())
|
||||
on_actionCheckUpdate_triggered();
|
||||
}
|
||||
|
||||
@ -321,7 +325,7 @@ void MainWindow::repopulateAccountsMenu()
|
||||
QAction *action = new QAction(profile.name, this);
|
||||
action->setData(account->username());
|
||||
action->setCheckable(true);
|
||||
if(active_username == account->username())
|
||||
if (active_username == account->username())
|
||||
{
|
||||
action->setChecked(true);
|
||||
}
|
||||
@ -339,7 +343,7 @@ void MainWindow::repopulateAccountsMenu()
|
||||
action->setCheckable(true);
|
||||
action->setIcon(QPixmap(":/icons/toolbar/noaccount").scaled(48, 48, Qt::KeepAspectRatio));
|
||||
action->setData("");
|
||||
if(active_username.isEmpty())
|
||||
if (active_username.isEmpty())
|
||||
{
|
||||
action->setChecked(true);
|
||||
}
|
||||
@ -356,10 +360,11 @@ void MainWindow::repopulateAccountsMenu()
|
||||
*/
|
||||
void MainWindow::changeActiveAccount()
|
||||
{
|
||||
QAction* sAction = (QAction*) sender();
|
||||
QAction *sAction = (QAction *)sender();
|
||||
// Profile's associated Mojang username
|
||||
// Will need to change when profiles are properly implemented
|
||||
if (sAction->data().type() != QVariant::Type::String) return;
|
||||
if (sAction->data().type() != QVariant::Type::String)
|
||||
return;
|
||||
|
||||
QVariant data = sAction->data();
|
||||
QString id = "";
|
||||
@ -390,7 +395,8 @@ void MainWindow::activeAccountChanged()
|
||||
}
|
||||
|
||||
// Set the icon to the "no account" icon.
|
||||
accountMenuButton->setIcon(QPixmap(":/icons/toolbar/noaccount").scaled(48, 48, Qt::KeepAspectRatio));
|
||||
accountMenuButton->setIcon(
|
||||
QPixmap(":/icons/toolbar/noaccount").scaled(48, 48, Qt::KeepAspectRatio));
|
||||
}
|
||||
|
||||
bool MainWindow::eventFilter(QObject *obj, QEvent *ev)
|
||||
@ -426,26 +432,28 @@ bool MainWindow::eventFilter(QObject *obj, QEvent *ev)
|
||||
void MainWindow::updateAvailable(QString repo, QString versionName, int versionId)
|
||||
{
|
||||
UpdateDialog dlg;
|
||||
UpdateAction action = (UpdateAction) dlg.exec();
|
||||
switch(action)
|
||||
UpdateAction action = (UpdateAction)dlg.exec();
|
||||
switch (action)
|
||||
{
|
||||
case UPDATE_LATER:
|
||||
QLOG_INFO() << "Update will be installed later.";
|
||||
break;
|
||||
case UPDATE_NOW:
|
||||
downloadUpdates(repo, versionId);
|
||||
break;
|
||||
case UPDATE_ONEXIT:
|
||||
downloadUpdates(repo, versionId, true);
|
||||
break;
|
||||
case UPDATE_LATER:
|
||||
QLOG_INFO() << "Update will be installed later.";
|
||||
break;
|
||||
case UPDATE_NOW:
|
||||
downloadUpdates(repo, versionId);
|
||||
break;
|
||||
case UPDATE_ONEXIT:
|
||||
downloadUpdates(repo, versionId, true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::downloadUpdates(QString repo, int versionId, bool installOnExit)
|
||||
{
|
||||
QLOG_INFO() << "Downloading updates.";
|
||||
// TODO: If the user chooses to update on exit, we should download updates in the background.
|
||||
// Doing so is a bit complicated, because we'd have to make sure it finished downloading before actually exiting MultiMC.
|
||||
// TODO: If the user chooses to update on exit, we should download updates in the
|
||||
// background.
|
||||
// Doing so is a bit complicated, because we'd have to make sure it finished downloading
|
||||
// before actually exiting MultiMC.
|
||||
ProgressDialog updateDlg(this);
|
||||
DownloadUpdateTask updateTask(repo, versionId, &updateDlg);
|
||||
// If the task succeeds, install the updates.
|
||||
@ -521,36 +529,44 @@ void MainWindow::on_actionAddInstance_triggered()
|
||||
{
|
||||
errorMsg += "An instance with the given directory name already exists.";
|
||||
CustomMessageBox::selectable(this, tr("Error"), errorMsg, QMessageBox::Warning)->show();
|
||||
break;
|
||||
return;
|
||||
}
|
||||
|
||||
case InstanceFactory::CantCreateDir:
|
||||
{
|
||||
errorMsg += "Failed to create the instance directory.";
|
||||
CustomMessageBox::selectable(this, tr("Error"), errorMsg, QMessageBox::Warning)->show();
|
||||
break;
|
||||
return;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
errorMsg += QString("Unknown instance loader error %1").arg(error);
|
||||
CustomMessageBox::selectable(this, tr("Error"), errorMsg, QMessageBox::Warning)->show();
|
||||
break;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<MojangAccountList> accounts = MMC->accounts();
|
||||
MojangAccountPtr account = accounts->activeAccount();
|
||||
if(account.get() != nullptr && account->accountStatus() != NotVerified)
|
||||
if (MMC->accounts()->anyAccountIsValid())
|
||||
{
|
||||
ProgressDialog loadDialog(this);
|
||||
auto update = newInstance->doUpdate(false);
|
||||
connect(update.get(), &Task::failed , [this](QString reason) {
|
||||
connect(update.get(), &Task::failed, [this](QString reason)
|
||||
{
|
||||
QString error = QString("Instance load failed: %1").arg(reason);
|
||||
CustomMessageBox::selectable(this, tr("Error"), error, QMessageBox::Warning)->show();
|
||||
CustomMessageBox::selectable(this, tr("Error"), error, QMessageBox::Warning)
|
||||
->show();
|
||||
});
|
||||
loadDialog.exec(update.get());
|
||||
}
|
||||
else
|
||||
{
|
||||
CustomMessageBox::selectable(
|
||||
this, tr("Error"),
|
||||
tr("MultiMC cannot download Minecraft or update instances unless you have at least "
|
||||
"one account added.\nPlease add your Mojang or Minecraft account."),
|
||||
QMessageBox::Warning)->show();
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::on_actionCopyInstance_triggered()
|
||||
@ -625,8 +641,14 @@ void MainWindow::on_actionChangeInstGroup_triggered()
|
||||
|
||||
bool ok = false;
|
||||
QString name(m_selectedInstance->group());
|
||||
name = QInputDialog::getText(this, tr("Group name"), tr("Enter a new group name."),
|
||||
QLineEdit::Normal, name, &ok);
|
||||
auto groups = MMC->instances()->getGroups();
|
||||
groups.insert(0, "");
|
||||
groups.sort(Qt::CaseInsensitive);
|
||||
int foo = groups.indexOf(name);
|
||||
|
||||
name = QInputDialog::getItem(this, tr("Group name"), tr("Enter a new group name."), groups,
|
||||
foo, true, &ok);
|
||||
name = name.simplified();
|
||||
if (ok)
|
||||
m_selectedInstance->setGroupPost(name);
|
||||
}
|
||||
@ -810,9 +832,11 @@ void MainWindow::doLaunch()
|
||||
if (accounts->count() <= 0)
|
||||
{
|
||||
// Tell the user they need to log in at least one account in order to play.
|
||||
auto reply = CustomMessageBox::selectable(this, tr("No Accounts"),
|
||||
tr("In order to play Minecraft, you must have at least one Mojang or Minecraft account logged in to MultiMC."
|
||||
"Would you like to open the account manager to add an account now?"),
|
||||
auto reply = CustomMessageBox::selectable(
|
||||
this, tr("No Accounts"),
|
||||
tr("In order to play Minecraft, you must have at least one Mojang or Minecraft "
|
||||
"account logged in to MultiMC."
|
||||
"Would you like to open the account manager to add an account now?"),
|
||||
QMessageBox::Information, QMessageBox::Yes | QMessageBox::No)->exec();
|
||||
|
||||
if (reply == QMessageBox::Yes)
|
||||
@ -825,7 +849,7 @@ void MainWindow::doLaunch()
|
||||
{
|
||||
// If no default account is set, ask the user which one to use.
|
||||
AccountSelectDialog selectDialog(tr("Which account would you like to use?"),
|
||||
AccountSelectDialog::GlobalDefaultCheckbox, this);
|
||||
AccountSelectDialog::GlobalDefaultCheckbox, this);
|
||||
|
||||
selectDialog.exec();
|
||||
|
||||
@ -842,7 +866,7 @@ void MainWindow::doLaunch()
|
||||
return;
|
||||
|
||||
// do the login. if the account has an access token, try to refresh it first.
|
||||
if(account->accountStatus() != NotVerified)
|
||||
if (account->accountStatus() != NotVerified)
|
||||
{
|
||||
// We'll need to validate the access token to make sure the account is still logged in.
|
||||
ProgressDialog progDialog(this);
|
||||
@ -851,7 +875,7 @@ void MainWindow::doLaunch()
|
||||
progDialog.exec(task.get());
|
||||
|
||||
auto status = account->accountStatus();
|
||||
if(status != NotVerified)
|
||||
if (status != NotVerified)
|
||||
{
|
||||
updateInstance(m_selectedInstance, account);
|
||||
}
|
||||
@ -859,20 +883,22 @@ void MainWindow::doLaunch()
|
||||
account->downgrade();
|
||||
return;
|
||||
}
|
||||
if (loginWithPassword(account, tr("Your account is currently not logged in. Please enter your password to log in again.")))
|
||||
if (loginWithPassword(account, tr("Your account is currently not logged in. Please enter "
|
||||
"your password to log in again.")))
|
||||
updateInstance(m_selectedInstance, account);
|
||||
}
|
||||
|
||||
bool MainWindow::loginWithPassword(MojangAccountPtr account, const QString& errorMsg)
|
||||
bool MainWindow::loginWithPassword(MojangAccountPtr account, const QString &errorMsg)
|
||||
{
|
||||
EditAccountDialog passDialog(errorMsg, this, EditAccountDialog::PasswordField);
|
||||
if (passDialog.exec() == QDialog::Accepted)
|
||||
{
|
||||
// To refresh the token, we just create an authenticate task with the given account and the user's password.
|
||||
// To refresh the token, we just create an authenticate task with the given account and
|
||||
// the user's password.
|
||||
ProgressDialog progDialog(this);
|
||||
auto task = account->login(passDialog.password());
|
||||
progDialog.exec(task.get());
|
||||
if(task->successful())
|
||||
if (task->successful())
|
||||
return true;
|
||||
else
|
||||
{
|
||||
@ -883,21 +909,20 @@ bool MainWindow::loginWithPassword(MojangAccountPtr account, const QString& erro
|
||||
return false;
|
||||
}
|
||||
|
||||
void MainWindow::updateInstance(BaseInstance* instance, MojangAccountPtr account)
|
||||
void MainWindow::updateInstance(BaseInstance *instance, MojangAccountPtr account)
|
||||
{
|
||||
bool only_prepare = account->accountStatus() != Online;
|
||||
auto updateTask = instance->doUpdate(only_prepare);
|
||||
if (!updateTask)
|
||||
{
|
||||
launchInstance(instance, account);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
ProgressDialog tDialog(this);
|
||||
connect(updateTask.get(), &Task::succeeded, [this, instance, account] { launchInstance(instance, account); });
|
||||
connect(updateTask.get(), SIGNAL(failed(QString)), SLOT(onGameUpdateError(QString)));
|
||||
tDialog.exec(updateTask.get());
|
||||
}
|
||||
ProgressDialog tDialog(this);
|
||||
connect(updateTask.get(), &Task::succeeded, [this, instance, account]
|
||||
{ launchInstance(instance, account); });
|
||||
connect(updateTask.get(), SIGNAL(failed(QString)), SLOT(onGameUpdateError(QString)));
|
||||
tDialog.exec(updateTask.get());
|
||||
}
|
||||
|
||||
void MainWindow::launchInstance(BaseInstance *instance, MojangAccountPtr account)
|
||||
@ -985,13 +1010,31 @@ void MainWindow::on_actionChangeInstMCVersion_triggered()
|
||||
this, tr("Are you sure?"),
|
||||
tr("This will remove any library/version customization you did previously. "
|
||||
"This includes things like Forge install and similar."),
|
||||
QMessageBox::Warning, QMessageBox::Ok, QMessageBox::Abort)->exec();
|
||||
QMessageBox::Warning, QMessageBox::Ok | QMessageBox::Abort,
|
||||
QMessageBox::Abort)->exec();
|
||||
|
||||
if (result != QMessageBox::Ok)
|
||||
return;
|
||||
}
|
||||
m_selectedInstance->setIntendedVersionId(vselect.selectedVersion()->descriptor());
|
||||
}
|
||||
if (!MMC->accounts()->anyAccountIsValid())
|
||||
{
|
||||
CustomMessageBox::selectable(
|
||||
this, tr("Error"),
|
||||
tr("MultiMC cannot download Minecraft or update instances unless you have at least "
|
||||
"one account added.\nPlease add your Mojang or Minecraft account."),
|
||||
QMessageBox::Warning)->show();
|
||||
return;
|
||||
}
|
||||
auto updateTask = m_selectedInstance->doUpdate(false /*only_prepare*/);
|
||||
if (!updateTask)
|
||||
{
|
||||
return;
|
||||
}
|
||||
ProgressDialog tDialog(this);
|
||||
connect(updateTask.get(), SIGNAL(failed(QString)), SLOT(onGameUpdateError(QString)));
|
||||
tDialog.exec(updateTask.get());
|
||||
}
|
||||
|
||||
void MainWindow::on_actionChangeInstLWJGLVersion_triggered()
|
||||
|
@ -36,8 +36,12 @@ AccountListDialog::AccountListDialog(QWidget *parent)
|
||||
ui->setupUi(this);
|
||||
|
||||
m_accounts = MMC->accounts();
|
||||
// TODO: Make the "Active?" column show checkboxes or radio buttons.
|
||||
|
||||
ui->listView->setModel(m_accounts.get());
|
||||
ui->listView->header()->setSectionResizeMode(QHeaderView::ResizeToContents);
|
||||
|
||||
// Expand the account column
|
||||
ui->listView->header()->setSectionResizeMode(1, QHeaderView::Stretch);
|
||||
|
||||
QItemSelectionModel* selectionModel = ui->listView->selectionModel();
|
||||
|
||||
|
@ -149,7 +149,7 @@ public:
|
||||
*/
|
||||
virtual SettingsObject &settings() const;
|
||||
|
||||
/// returns a valid update task if update is needed, NULL otherwise
|
||||
/// returns a valid update task
|
||||
virtual std::shared_ptr<Task> doUpdate(bool only_prepare) = 0;
|
||||
|
||||
/// returns a valid minecraft process, ready for launch with the given account.
|
||||
|
@ -54,11 +54,9 @@ void OneSixUpdate::executeTask()
|
||||
|
||||
if (m_only_prepare)
|
||||
{
|
||||
if (m_inst->shouldUpdate())
|
||||
{
|
||||
emitFailed("Unable to update instance in offline mode.");
|
||||
return;
|
||||
}
|
||||
/*
|
||||
* FIXME: in offline mode, do not proceed!
|
||||
*/
|
||||
setStatus("Testing the Java installation.");
|
||||
QString java_path = m_inst->settings().get("JavaPath").toString();
|
||||
|
||||
@ -243,6 +241,7 @@ void OneSixUpdate::assetIndexFinished()
|
||||
auto objectDL = MD5EtagDownload::make(
|
||||
QUrl("http://" + URLConstants::RESOURCE_BASE + objectName),
|
||||
objectFile.filePath());
|
||||
objectDL->m_total_progress = object.size;
|
||||
dls.append(objectDL);
|
||||
}
|
||||
}
|
||||
|
@ -27,6 +27,7 @@
|
||||
#include "logger/QsLog.h"
|
||||
|
||||
#include "logic/auth/MojangAccount.h"
|
||||
#include <pathutils.h>
|
||||
|
||||
#define ACCOUNT_LIST_FORMAT_VERSION 2
|
||||
|
||||
@ -265,11 +266,6 @@ bool MojangAccountList::loadList(const QString &filePath)
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!QDir::current().exists(path))
|
||||
{
|
||||
QDir::current().mkpath(path);
|
||||
}
|
||||
|
||||
QFile file(path);
|
||||
|
||||
// Try to open the file and fail if we can't.
|
||||
@ -351,9 +347,16 @@ bool MojangAccountList::saveList(const QString &filePath)
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!QDir::current().exists(path))
|
||||
// make sure the parent folder exists
|
||||
if(!ensureFilePathExists(path))
|
||||
return false;
|
||||
|
||||
// make sure the file wasn't overwritten with a folder before (fixes a bug)
|
||||
QFileInfo finfo(path);
|
||||
if(finfo.isDir())
|
||||
{
|
||||
QDir::current().mkpath(path);
|
||||
QDir badDir(path);
|
||||
badDir.removeRecursively();
|
||||
}
|
||||
|
||||
QLOG_INFO() << "Writing account list to" << path;
|
||||
@ -411,3 +414,13 @@ void MojangAccountList::setListFilePath(QString path, bool autosave)
|
||||
m_listFilePath = path;
|
||||
m_autosave = autosave;
|
||||
}
|
||||
|
||||
bool MojangAccountList::anyAccountIsValid()
|
||||
{
|
||||
for(auto account:m_accounts)
|
||||
{
|
||||
if(account->accountStatus() != NotVerified)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
@ -126,6 +126,11 @@ public:
|
||||
* If the username given is an empty string, sets the active account to nothing.
|
||||
*/
|
||||
virtual void setActiveAccount(const QString &username);
|
||||
|
||||
/*!
|
||||
* Returns true if any of the account is at least Validated
|
||||
*/
|
||||
bool anyAccountIsValid();
|
||||
|
||||
signals:
|
||||
/*!
|
||||
|
@ -117,6 +117,11 @@ void InstanceList::groupChanged()
|
||||
saveGroupList();
|
||||
}
|
||||
|
||||
QStringList InstanceList::getGroups()
|
||||
{
|
||||
return m_groups.toList();
|
||||
}
|
||||
|
||||
void InstanceList::saveGroupList()
|
||||
{
|
||||
QString groupFileName = m_instDir + "/instgroups.json";
|
||||
@ -126,7 +131,7 @@ void InstanceList::saveGroupList()
|
||||
if (!groupFile.open(QIODevice::WriteOnly | QIODevice::Truncate))
|
||||
{
|
||||
// An error occurred. Ignore it.
|
||||
QLOG_ERROR() << "Failed to read instance group file.";
|
||||
QLOG_ERROR() << "Failed to save instance group file.";
|
||||
return;
|
||||
}
|
||||
QTextStream out(&groupFile);
|
||||
@ -137,6 +142,10 @@ void InstanceList::saveGroupList()
|
||||
QString group = instance->group();
|
||||
if (group.isEmpty())
|
||||
continue;
|
||||
|
||||
// keep a list/set of groups for choosing
|
||||
m_groups.insert(group);
|
||||
|
||||
if (!groupMap.count(group))
|
||||
{
|
||||
QSet<QString> set;
|
||||
@ -253,6 +262,9 @@ void InstanceList::loadGroupList(QMap<QString, QString> &groupMap)
|
||||
continue;
|
||||
}
|
||||
|
||||
// keep a list/set of groups for choosing
|
||||
m_groups.insert(groupName);
|
||||
|
||||
// Iterate through the list of instances in the group.
|
||||
QJsonArray instancesArray = groupObj.value("instances").toArray();
|
||||
|
||||
|
@ -17,6 +17,7 @@
|
||||
|
||||
#include <QObject>
|
||||
#include <QAbstractListModel>
|
||||
#include <QSet>
|
||||
#include "categorizedsortfilterproxymodel.h"
|
||||
#include <QIcon>
|
||||
|
||||
@ -97,6 +98,9 @@ public:
|
||||
InstancePtr getInstanceById(QString id) const;
|
||||
|
||||
QModelIndex getInstanceIndexById(const QString &id) const;
|
||||
|
||||
// FIXME: instead of iterating through all instances and forming a set, keep the set around
|
||||
QStringList getGroups();
|
||||
signals:
|
||||
void dataIsInvalid();
|
||||
|
||||
@ -116,6 +120,7 @@ private:
|
||||
protected:
|
||||
QString m_instDir;
|
||||
QList<InstancePtr> m_instances;
|
||||
QSet<QString> m_groups;
|
||||
};
|
||||
|
||||
class InstanceProxyModel : public KCategorizedSortFilterProxyModel
|
||||
|
@ -42,7 +42,9 @@ void ByteArrayDownload::start()
|
||||
|
||||
void ByteArrayDownload::downloadProgress(qint64 bytesReceived, qint64 bytesTotal)
|
||||
{
|
||||
emit progress(index_within_job, bytesReceived, bytesTotal);
|
||||
m_total_progress = bytesTotal;
|
||||
m_progress = bytesReceived;
|
||||
emit progress(m_index_within_job, bytesReceived, bytesTotal);
|
||||
}
|
||||
|
||||
void ByteArrayDownload::downloadError(QNetworkReply::NetworkError error)
|
||||
@ -62,14 +64,14 @@ void ByteArrayDownload::downloadFinished()
|
||||
m_status = Job_Finished;
|
||||
m_data = m_reply->readAll();
|
||||
m_reply.reset();
|
||||
emit succeeded(index_within_job);
|
||||
emit succeeded(m_index_within_job);
|
||||
return;
|
||||
}
|
||||
// else the download failed
|
||||
else
|
||||
{
|
||||
m_reply.reset();
|
||||
emit failed(index_within_job);
|
||||
emit failed(m_index_within_job);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -35,14 +35,14 @@ void CacheDownload::start()
|
||||
{
|
||||
if (!m_entry->stale)
|
||||
{
|
||||
emit succeeded(index_within_job);
|
||||
emit succeeded(m_index_within_job);
|
||||
return;
|
||||
}
|
||||
m_output_file.setFileName(m_target_path);
|
||||
// if there already is a file and md5 checking is in effect and it can be opened
|
||||
if (!ensureFilePathExists(m_target_path))
|
||||
{
|
||||
emit failed(index_within_job);
|
||||
emit failed(m_index_within_job);
|
||||
return;
|
||||
}
|
||||
QLOG_INFO() << "Downloading " << m_url.toString();
|
||||
@ -69,7 +69,9 @@ void CacheDownload::start()
|
||||
|
||||
void CacheDownload::downloadProgress(qint64 bytesReceived, qint64 bytesTotal)
|
||||
{
|
||||
emit progress(index_within_job, bytesReceived, bytesTotal);
|
||||
m_total_progress = bytesTotal;
|
||||
m_progress = bytesReceived;
|
||||
emit progress(m_index_within_job, bytesReceived, bytesTotal);
|
||||
}
|
||||
|
||||
void CacheDownload::downloadError(QNetworkReply::NetworkError error)
|
||||
@ -116,7 +118,7 @@ void CacheDownload::downloadFinished()
|
||||
MMC->metacache()->updateEntry(m_entry);
|
||||
|
||||
m_reply.reset();
|
||||
emit succeeded(index_within_job);
|
||||
emit succeeded(m_index_within_job);
|
||||
return;
|
||||
}
|
||||
// else the download failed
|
||||
@ -125,7 +127,7 @@ void CacheDownload::downloadFinished()
|
||||
m_output_file.close();
|
||||
m_output_file.remove();
|
||||
m_reply.reset();
|
||||
emit failed(index_within_job);
|
||||
emit failed(m_index_within_job);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -140,7 +142,7 @@ void CacheDownload::downloadReadyRead()
|
||||
* Can't open the file... the job failed
|
||||
*/
|
||||
m_reply->abort();
|
||||
emit failed(index_within_job);
|
||||
emit failed(m_index_within_job);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -68,7 +68,7 @@ void ForgeMirrors::deferToFixedList()
|
||||
"https://www.creeperhost.net/link.php?id=1",
|
||||
"http://new.creeperrepo.net/forge/maven/"});
|
||||
injectDownloads();
|
||||
emit succeeded(index_within_job);
|
||||
emit succeeded(m_index_within_job);
|
||||
}
|
||||
|
||||
void ForgeMirrors::parseMirrorList()
|
||||
@ -88,7 +88,7 @@ void ForgeMirrors::parseMirrorList()
|
||||
if(!m_mirrors.size())
|
||||
deferToFixedList();
|
||||
injectDownloads();
|
||||
emit succeeded(index_within_job);
|
||||
emit succeeded(m_index_within_job);
|
||||
}
|
||||
|
||||
void ForgeMirrors::injectDownloads()
|
||||
@ -108,7 +108,9 @@ void ForgeMirrors::injectDownloads()
|
||||
|
||||
void ForgeMirrors::downloadProgress(qint64 bytesReceived, qint64 bytesTotal)
|
||||
{
|
||||
emit progress(index_within_job, bytesReceived, bytesTotal);
|
||||
m_total_progress = bytesTotal;
|
||||
m_progress = bytesReceived;
|
||||
emit progress(m_index_within_job, bytesReceived, bytesTotal);
|
||||
}
|
||||
|
||||
void ForgeMirrors::downloadReadyRead()
|
||||
|
@ -44,20 +44,20 @@ void ForgeXzDownload::start()
|
||||
if (!m_entry->stale)
|
||||
{
|
||||
m_status = Job_Finished;
|
||||
emit succeeded(index_within_job);
|
||||
emit succeeded(m_index_within_job);
|
||||
return;
|
||||
}
|
||||
// can we actually create the real, final file?
|
||||
if (!ensureFilePathExists(m_target_path))
|
||||
{
|
||||
m_status = Job_Failed;
|
||||
emit failed(index_within_job);
|
||||
emit failed(m_index_within_job);
|
||||
return;
|
||||
}
|
||||
if (m_mirrors.empty())
|
||||
{
|
||||
m_status = Job_Failed;
|
||||
emit failed(index_within_job);
|
||||
emit failed(m_index_within_job);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -80,7 +80,9 @@ void ForgeXzDownload::start()
|
||||
|
||||
void ForgeXzDownload::downloadProgress(qint64 bytesReceived, qint64 bytesTotal)
|
||||
{
|
||||
emit progress(index_within_job, bytesReceived, bytesTotal);
|
||||
m_total_progress = bytesTotal;
|
||||
m_progress = bytesReceived;
|
||||
emit progress(m_index_within_job, bytesReceived, bytesTotal);
|
||||
}
|
||||
|
||||
void ForgeXzDownload::downloadError(QNetworkReply::NetworkError error)
|
||||
@ -100,7 +102,7 @@ void ForgeXzDownload::failAndTryNextMirror()
|
||||
m_mirror_index = next;
|
||||
|
||||
updateUrl();
|
||||
emit failed(index_within_job);
|
||||
emit failed(m_index_within_job);
|
||||
}
|
||||
|
||||
void ForgeXzDownload::updateUrl()
|
||||
@ -148,7 +150,7 @@ void ForgeXzDownload::downloadFinished()
|
||||
m_status = Job_Failed;
|
||||
m_pack200_xz_file.remove();
|
||||
m_reply.reset();
|
||||
emit failed(index_within_job);
|
||||
emit failed(m_index_within_job);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -175,7 +177,7 @@ void ForgeXzDownload::downloadReadyRead()
|
||||
* Can't open the file... the job failed
|
||||
*/
|
||||
m_reply->abort();
|
||||
emit failed(index_within_job);
|
||||
emit failed(m_index_within_job);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -347,5 +349,5 @@ void ForgeXzDownload::decompressAndInstall()
|
||||
MMC->metacache()->updateEntry(m_entry);
|
||||
|
||||
m_reply.reset();
|
||||
emit succeeded(index_within_job);
|
||||
emit succeeded(m_index_within_job);
|
||||
}
|
||||
|
@ -44,7 +44,7 @@ void MD5EtagDownload::start()
|
||||
if (m_check_md5 && hash == m_expected_md5)
|
||||
{
|
||||
QLOG_INFO() << "Skipping " << m_url.toString() << ": md5 match.";
|
||||
emit succeeded(index_within_job);
|
||||
emit succeeded(m_index_within_job);
|
||||
return;
|
||||
}
|
||||
else
|
||||
@ -54,7 +54,7 @@ void MD5EtagDownload::start()
|
||||
}
|
||||
if (!ensureFilePathExists(filename))
|
||||
{
|
||||
emit failed(index_within_job);
|
||||
emit failed(m_index_within_job);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -68,7 +68,7 @@ void MD5EtagDownload::start()
|
||||
// Plus, this way, we don't end up starting a download for a file we can't open.
|
||||
if (!m_output_file.open(QIODevice::WriteOnly))
|
||||
{
|
||||
emit failed(index_within_job);
|
||||
emit failed(m_index_within_job);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -86,7 +86,9 @@ void MD5EtagDownload::start()
|
||||
|
||||
void MD5EtagDownload::downloadProgress(qint64 bytesReceived, qint64 bytesTotal)
|
||||
{
|
||||
emit progress(index_within_job, bytesReceived, bytesTotal);
|
||||
m_total_progress = bytesTotal;
|
||||
m_progress = bytesReceived;
|
||||
emit progress(m_index_within_job, bytesReceived, bytesTotal);
|
||||
}
|
||||
|
||||
void MD5EtagDownload::downloadError(QNetworkReply::NetworkError error)
|
||||
@ -107,7 +109,7 @@ void MD5EtagDownload::downloadFinished()
|
||||
|
||||
QLOG_INFO() << "Finished " << m_url.toString() << " got " << m_reply->rawHeader("ETag").constData();
|
||||
m_reply.reset();
|
||||
emit succeeded(index_within_job);
|
||||
emit succeeded(m_index_within_job);
|
||||
return;
|
||||
}
|
||||
// else the download failed
|
||||
@ -115,7 +117,7 @@ void MD5EtagDownload::downloadFinished()
|
||||
{
|
||||
m_output_file.close();
|
||||
m_reply.reset();
|
||||
emit failed(index_within_job);
|
||||
emit failed(m_index_within_job);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -130,7 +132,7 @@ void MD5EtagDownload::downloadReadyRead()
|
||||
* Can't open the file... the job failed
|
||||
*/
|
||||
m_reply->abort();
|
||||
emit failed(index_within_job);
|
||||
emit failed(m_index_within_job);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -38,6 +38,19 @@ protected:
|
||||
public:
|
||||
virtual ~NetAction() {};
|
||||
|
||||
public:
|
||||
virtual qint64 totalProgress() const
|
||||
{
|
||||
return m_total_progress;
|
||||
}
|
||||
virtual qint64 currentProgress() const
|
||||
{
|
||||
return m_progress;
|
||||
}
|
||||
virtual qint64 numberOfFailures() const
|
||||
{
|
||||
return m_failures;
|
||||
}
|
||||
public:
|
||||
/// the network reply
|
||||
std::shared_ptr<QNetworkReply> m_reply;
|
||||
@ -46,10 +59,16 @@ public:
|
||||
QUrl m_url;
|
||||
|
||||
/// The file's status
|
||||
JobStatus m_status;
|
||||
JobStatus m_status = Job_NotStarted;
|
||||
|
||||
/// index within the parent job
|
||||
int index_within_job = 0;
|
||||
int m_index_within_job = 0;
|
||||
|
||||
qint64 m_progress = 0;
|
||||
qint64 m_total_progress = 1;
|
||||
|
||||
/// number of failures up to this point
|
||||
int m_failures = 0;
|
||||
|
||||
signals:
|
||||
void started(int index);
|
||||
|
@ -36,10 +36,16 @@ public:
|
||||
template <typename T> bool addNetAction(T action)
|
||||
{
|
||||
NetActionPtr base = std::static_pointer_cast<NetAction>(action);
|
||||
base->index_within_job = downloads.size();
|
||||
base->m_index_within_job = downloads.size();
|
||||
downloads.append(action);
|
||||
parts_progress.append(part_info());
|
||||
total_progress++;
|
||||
part_info pi;
|
||||
{
|
||||
pi.current_progress = base->currentProgress();
|
||||
pi.total_progress = base->totalProgress();
|
||||
pi.failures = base->numberOfFailures();
|
||||
}
|
||||
parts_progress.append(pi);
|
||||
total_progress += pi.total_progress;
|
||||
// if this is already running, the action needs to be started right away!
|
||||
if (isRunning())
|
||||
{
|
||||
|
@ -78,7 +78,9 @@ bool PasteUpload::parseResult(QJsonDocument doc, QString *parseError)
|
||||
parseError = new QString(object.value("error").toString());
|
||||
return false;
|
||||
}
|
||||
// FIXME: not the place for GUI things.
|
||||
QString pasteUrl = object.value("paste").toObject().value("link").toString();
|
||||
QDesktopServices::openUrl(pasteUrl);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -1,175 +0,0 @@
|
||||
/* Copyright 2013 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "S3ListBucket.h"
|
||||
#include "MultiMC.h"
|
||||
#include "logger/QsLog.h"
|
||||
#include <QUrlQuery>
|
||||
#include <qxmlstream.h>
|
||||
#include <QDomDocument>
|
||||
|
||||
inline QDomElement getDomElementByTagName(QDomElement parent, QString tagname)
|
||||
{
|
||||
QDomNodeList elementList = parent.elementsByTagName(tagname);
|
||||
if (elementList.count())
|
||||
return elementList.at(0).toElement();
|
||||
else
|
||||
return QDomElement();
|
||||
}
|
||||
|
||||
S3ListBucket::S3ListBucket(QUrl url) : NetAction()
|
||||
{
|
||||
m_url = url;
|
||||
m_status = Job_NotStarted;
|
||||
}
|
||||
|
||||
void S3ListBucket::start()
|
||||
{
|
||||
QUrl finalUrl = m_url;
|
||||
if (current_marker.size())
|
||||
{
|
||||
QUrlQuery query;
|
||||
query.addQueryItem("marker", current_marker);
|
||||
finalUrl.setQuery(query);
|
||||
}
|
||||
QNetworkRequest request(finalUrl);
|
||||
request.setHeader(QNetworkRequest::UserAgentHeader, "MultiMC/5.0 (Uncached)");
|
||||
auto worker = MMC->qnam();
|
||||
QNetworkReply *rep = worker->get(request);
|
||||
|
||||
m_reply = std::shared_ptr<QNetworkReply>(rep);
|
||||
connect(rep, SIGNAL(downloadProgress(qint64, qint64)),
|
||||
SLOT(downloadProgress(qint64, qint64)));
|
||||
connect(rep, SIGNAL(finished()), SLOT(downloadFinished()));
|
||||
connect(rep, SIGNAL(error(QNetworkReply::NetworkError)),
|
||||
SLOT(downloadError(QNetworkReply::NetworkError)));
|
||||
connect(rep, SIGNAL(readyRead()), SLOT(downloadReadyRead()));
|
||||
}
|
||||
|
||||
void S3ListBucket::downloadProgress(qint64 bytesReceived, qint64 bytesTotal)
|
||||
{
|
||||
emit progress(index_within_job, bytesSoFar + bytesReceived, bytesSoFar + bytesTotal);
|
||||
}
|
||||
|
||||
void S3ListBucket::downloadError(QNetworkReply::NetworkError error)
|
||||
{
|
||||
// error happened during download.
|
||||
QLOG_ERROR() << "Error getting URL:" << m_url.toString().toLocal8Bit()
|
||||
<< "Network error: " << error;
|
||||
m_status = Job_Failed;
|
||||
}
|
||||
|
||||
void S3ListBucket::processValidReply()
|
||||
{
|
||||
QLOG_TRACE() << "GOT: " << m_url.toString() << " marker:" << current_marker;
|
||||
auto readContents = [&](QXmlStreamReader & xml)
|
||||
{
|
||||
QString Key, ETag, Size;
|
||||
while (!(xml.tokenType() == QXmlStreamReader::EndElement && xml.name() == "Contents"))
|
||||
{
|
||||
if (xml.tokenType() == QXmlStreamReader::StartElement)
|
||||
{
|
||||
if (xml.name() == "Key")
|
||||
{
|
||||
Key = xml.readElementText();
|
||||
}
|
||||
if (xml.name() == "ETag")
|
||||
{
|
||||
ETag = xml.readElementText();
|
||||
}
|
||||
if (xml.name() == "Size")
|
||||
{
|
||||
Size = xml.readElementText();
|
||||
}
|
||||
}
|
||||
xml.readNext();
|
||||
}
|
||||
if (xml.error() != QXmlStreamReader::NoError)
|
||||
return;
|
||||
objects.append({Key, ETag, Size.toLongLong()});
|
||||
};
|
||||
|
||||
// nothing went wrong...
|
||||
QByteArray ba = m_reply->readAll();
|
||||
|
||||
QString xmlErrorMsg;
|
||||
|
||||
bool is_truncated = false;
|
||||
QXmlStreamReader xml(ba);
|
||||
while (!xml.atEnd() && !xml.hasError())
|
||||
{
|
||||
/* Read next element.*/
|
||||
QXmlStreamReader::TokenType token = xml.readNext();
|
||||
/* If token is just StartDocument, we'll go to next.*/
|
||||
if (token == QXmlStreamReader::StartDocument)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (token == QXmlStreamReader::StartElement)
|
||||
{
|
||||
/* If it's named person, we'll dig the information from there.*/
|
||||
if (xml.name() == "Contents")
|
||||
{
|
||||
readContents(xml);
|
||||
}
|
||||
else if (xml.name() == "IsTruncated")
|
||||
{
|
||||
is_truncated = (xml.readElementText() == "true");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (xml.hasError())
|
||||
{
|
||||
QLOG_ERROR() << "Failed to process s3.amazonaws.com/Minecraft.Resources. XML error:"
|
||||
<< xml.errorString() << ba;
|
||||
emit failed(index_within_job);
|
||||
return;
|
||||
}
|
||||
if (is_truncated)
|
||||
{
|
||||
current_marker = objects.last().Key;
|
||||
bytesSoFar += m_reply->size();
|
||||
m_reply.reset();
|
||||
start();
|
||||
}
|
||||
else
|
||||
{
|
||||
m_status = Job_Finished;
|
||||
m_reply.reset();
|
||||
emit succeeded(index_within_job);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
void S3ListBucket::downloadFinished()
|
||||
{
|
||||
// if the download succeeded
|
||||
if (m_status != Job_Failed)
|
||||
{
|
||||
processValidReply();
|
||||
}
|
||||
// else the download failed
|
||||
else
|
||||
{
|
||||
m_reply.reset();
|
||||
emit failed(index_within_job);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void S3ListBucket::downloadReadyRead()
|
||||
{
|
||||
// ~_~
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
/* Copyright 2013 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "NetAction.h"
|
||||
|
||||
struct S3Object
|
||||
{
|
||||
QString Key;
|
||||
QString ETag;
|
||||
qlonglong size;
|
||||
};
|
||||
|
||||
typedef std::shared_ptr<class S3ListBucket> S3ListBucketPtr;
|
||||
class S3ListBucket : public NetAction
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
S3ListBucket(QUrl url);
|
||||
static S3ListBucketPtr make(QUrl url)
|
||||
{
|
||||
return S3ListBucketPtr(new S3ListBucket(url));
|
||||
}
|
||||
|
||||
public:
|
||||
QList<S3Object> objects;
|
||||
|
||||
public
|
||||
slots:
|
||||
virtual void start() override;
|
||||
|
||||
protected
|
||||
slots:
|
||||
virtual void downloadProgress(qint64 bytesReceived, qint64 bytesTotal) override;
|
||||
virtual void downloadError(QNetworkReply::NetworkError error) override;
|
||||
virtual void downloadFinished() override;
|
||||
virtual void downloadReadyRead() override;
|
||||
|
||||
private:
|
||||
void processValidReply();
|
||||
|
||||
private:
|
||||
qint64 bytesSoFar = 0;
|
||||
QString current_marker;
|
||||
};
|
@ -56,9 +56,7 @@ class UpdateScriptFile
|
||||
}
|
||||
};
|
||||
|
||||
/** Stores information about the packages and files included
|
||||
* in an update, parsed from an XML file.
|
||||
*/
|
||||
/** Stores information about the files included in an update, parsed from an XML file. */
|
||||
class UpdateScript
|
||||
{
|
||||
public:
|
||||
|
@ -142,7 +142,7 @@ void UpdaterOptions::parse(int argc, char** argv)
|
||||
showVersion = parser.getFlag("version");
|
||||
forceElevated = parser.getFlag("force-elevated");
|
||||
autoClose = parser.getFlag("auto-close");
|
||||
|
||||
|
||||
if (installDir.empty())
|
||||
{
|
||||
// if no --install-dir argument is present, try parsing
|
||||
@ -152,3 +152,4 @@ void UpdaterOptions::parse(int argc, char** argv)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -5,21 +5,19 @@ if (APPLE)
|
||||
set(HELPER_SHARED_SOURCES ../StlSymbolsLeopard.cpp)
|
||||
endif()
|
||||
|
||||
# Create helper binaries for unit tests
|
||||
add_executable(oldapp
|
||||
old_app.cpp
|
||||
${HELPER_SHARED_SOURCES}
|
||||
)
|
||||
add_executable(newapp
|
||||
new_app.cpp
|
||||
${HELPER_SHARED_SOURCES}
|
||||
)
|
||||
# # Create helper binaries for unit tests
|
||||
# add_executable(oldapp
|
||||
# old_app.cpp
|
||||
# ${HELPER_SHARED_SOURCES}
|
||||
# )
|
||||
# add_executable(newapp
|
||||
# new_app.cpp
|
||||
# ${HELPER_SHARED_SOURCES}
|
||||
# )
|
||||
|
||||
# Install data files required by unit tests
|
||||
set(TEST_FILES
|
||||
file_list.xml
|
||||
v2_file_list.xml
|
||||
test-update.rb
|
||||
)
|
||||
|
||||
foreach(TEST_FILE ${TEST_FILES})
|
||||
@ -40,12 +38,6 @@ macro(ADD_UPDATER_TEST CLASS)
|
||||
endif()
|
||||
endmacro()
|
||||
|
||||
#add_updater_test(TestUpdateScript)
|
||||
add_updater_test(TestParseScript)
|
||||
add_updater_test(TestUpdaterOptions)
|
||||
add_updater_test(TestFileUtils)
|
||||
|
||||
# Add updater that that performs a complete update install
|
||||
# and checks the result
|
||||
#find_program(RUBY_BIN ruby)
|
||||
#add_test(updater_TestUpdateInstall ${RUBY_BIN} test-update.rb)
|
||||
|
||||
|
24
mmc_updater/src/tests/TestParseScript.cpp
Normal file
24
mmc_updater/src/tests/TestParseScript.cpp
Normal file
@ -0,0 +1,24 @@
|
||||
#include "TestParseScript.h"
|
||||
|
||||
#include "TestUtils.h"
|
||||
#include "UpdateScript.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <algorithm>
|
||||
|
||||
void TestParseScript::testParse()
|
||||
{
|
||||
UpdateScript script;
|
||||
|
||||
script.parse("file_list.xml");
|
||||
|
||||
TEST_COMPARE(script.isValid(),true);
|
||||
}
|
||||
|
||||
int main(int,char**)
|
||||
{
|
||||
TestList<TestParseScript> tests;
|
||||
tests.addTest(&TestParseScript::testParse);
|
||||
return TestUtils::runTest(tests);
|
||||
}
|
||||
|
8
mmc_updater/src/tests/TestParseScript.h
Normal file
8
mmc_updater/src/tests/TestParseScript.h
Normal file
@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
class TestParseScript
|
||||
{
|
||||
public:
|
||||
void testParse();
|
||||
};
|
||||
|
@ -1,27 +0,0 @@
|
||||
#include "TestUpdateScript.h"
|
||||
|
||||
#include "TestUtils.h"
|
||||
#include "UpdateScript.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <algorithm>
|
||||
|
||||
void TestUpdateScript::testV2Script()
|
||||
{
|
||||
UpdateScript newFormat;
|
||||
UpdateScript oldFormat;
|
||||
|
||||
newFormat.parse("file_list.xml");
|
||||
oldFormat.parse("v2_file_list.xml");
|
||||
|
||||
TEST_COMPARE(newFormat.filesToInstall(),oldFormat.filesToInstall());
|
||||
TEST_COMPARE(newFormat.filesToUninstall(),oldFormat.filesToUninstall());
|
||||
}
|
||||
|
||||
int main(int,char**)
|
||||
{
|
||||
TestList<TestUpdateScript> tests;
|
||||
tests.addTest(&TestUpdateScript::testV2Script);
|
||||
return TestUtils::runTest(tests);
|
||||
}
|
||||
|
@ -1,8 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
class TestUpdateScript
|
||||
{
|
||||
public:
|
||||
void testV2Script();
|
||||
};
|
||||
|
@ -1,20 +1,5 @@
|
||||
<?xml version="1.0"?>
|
||||
<update version="3">
|
||||
<targetVersion>2.0</targetVersion>
|
||||
<platform>Test</platform>
|
||||
<dependencies>
|
||||
<!-- The new updater is standalone and has no dependencies,
|
||||
except for standard system libraries.
|
||||
!-->
|
||||
</dependencies>
|
||||
<packages>
|
||||
<package>
|
||||
<name>app-pkg</name>
|
||||
<hash>$APP_PACKAGE_HASH</hash>
|
||||
<size>$APP_PACKAGE_SIZE</size>
|
||||
<source>http://some/dummy/URL</source>
|
||||
</package>
|
||||
</packages>
|
||||
<install>
|
||||
<file>
|
||||
<name>$APP_FILENAME</name>
|
||||
|
@ -1,218 +0,0 @@
|
||||
#!/usr/bin/ruby
|
||||
|
||||
require 'fileutils.rb'
|
||||
require 'find'
|
||||
require 'rbconfig'
|
||||
require 'optparse'
|
||||
|
||||
# Install directory - this contains a space to check
|
||||
# for correct escaping of paths when passing comamnd
|
||||
# line arguments under Windows
|
||||
INSTALL_DIR = File.expand_path("install dir/")
|
||||
PACKAGE_DIR = File.expand_path("package-dir/")
|
||||
PACKAGE_SRC_DIR = File.expand_path("package-src-dir/")
|
||||
IS_WINDOWS = RbConfig::CONFIG['host_os'] =~ /mswin|mingw/
|
||||
|
||||
if IS_WINDOWS
|
||||
OLDAPP_NAME = "oldapp.exe"
|
||||
NEWAPP_NAME = "newapp.exe"
|
||||
APP_NAME = "app.exe"
|
||||
UPDATER_NAME = "updater.exe"
|
||||
ZIP_TOOL = File.expand_path("../zip-tool.exe")
|
||||
else
|
||||
OLDAPP_NAME = "oldapp"
|
||||
NEWAPP_NAME = "newapp"
|
||||
APP_NAME = "app"
|
||||
UPDATER_NAME = "updater"
|
||||
ZIP_TOOL = File.expand_path("../zip-tool")
|
||||
end
|
||||
|
||||
file_list_vars = {
|
||||
"APP_FILENAME" => APP_NAME,
|
||||
"UPDATER_FILENAME" => UPDATER_NAME
|
||||
}
|
||||
|
||||
def replace_vars(src_file,dest_file,vars)
|
||||
content = File.read(src_file)
|
||||
vars.each do |key,value|
|
||||
content.gsub! "$#{key}",value
|
||||
end
|
||||
File.open(dest_file,'w') do |file|
|
||||
file.print content
|
||||
end
|
||||
end
|
||||
|
||||
# Returns true if |src_file| and |dest_file| have the same contents, type
|
||||
# and permissions or false otherwise
|
||||
def compare_files(src_file, dest_file)
|
||||
if File.ftype(src_file) != File.ftype(dest_file)
|
||||
$stderr.puts "Type of file #{src_file} and #{dest_file} differ"
|
||||
return false
|
||||
end
|
||||
|
||||
if File.file?(src_file) && !FileUtils.identical?(src_file, dest_file)
|
||||
$stderr.puts "Contents of file #{src_file} and #{dest_file} differ"
|
||||
return false
|
||||
end
|
||||
|
||||
src_stat = File.stat(src_file)
|
||||
dest_stat = File.stat(dest_file)
|
||||
|
||||
if src_stat.mode != dest_stat.mode
|
||||
$stderr.puts "Permissions of #{src_file} and #{dest_file} differ"
|
||||
return false
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
# Compares the contents of two directories and returns a map of (file path => change type)
|
||||
# for files and directories which differ between the two
|
||||
def compare_dirs(src_dir, dest_dir)
|
||||
src_dir += '/' if !src_dir.end_with?('/')
|
||||
dest_dir += '/' if !dest_dir.end_with?('/')
|
||||
|
||||
src_file_map = {}
|
||||
Find.find(src_dir) do |src_file|
|
||||
src_file = src_file[src_dir.length..-1]
|
||||
src_file_map[src_file] = nil
|
||||
end
|
||||
|
||||
change_map = {}
|
||||
Find.find(dest_dir) do |dest_file|
|
||||
dest_file = dest_file[dest_dir.length..-1]
|
||||
|
||||
if !src_file_map.include?(dest_file)
|
||||
change_map[dest_file] = :deleted
|
||||
elsif !compare_files("#{src_dir}/#{dest_file}", "#{dest_dir}/#{dest_file}")
|
||||
change_map[dest_file] = :updated
|
||||
end
|
||||
|
||||
src_file_map.delete(dest_file)
|
||||
end
|
||||
|
||||
src_file_map.each do |file|
|
||||
change_map[file] = :added
|
||||
end
|
||||
|
||||
return change_map
|
||||
end
|
||||
|
||||
def create_test_file(name, content)
|
||||
File.open(name, 'w') do |file|
|
||||
file.puts content
|
||||
end
|
||||
return name
|
||||
end
|
||||
|
||||
force_elevation = false
|
||||
run_in_debugger = false
|
||||
|
||||
OptionParser.new do |parser|
|
||||
parser.on("-f","--force-elevated","Force the updater to elevate itself") do
|
||||
force_elevation = true
|
||||
end
|
||||
parser.on("-d","--debug","Run the updater under GDB") do
|
||||
run_in_debugger = true
|
||||
end
|
||||
end.parse!
|
||||
|
||||
# copy 'src' to 'dest', preserving the attributes
|
||||
# of 'src'
|
||||
def copy_file(src, dest)
|
||||
FileUtils.cp src, dest, :preserve => true
|
||||
end
|
||||
|
||||
# Remove the install and package dirs if they
|
||||
# already exist
|
||||
FileUtils.rm_rf(INSTALL_DIR)
|
||||
FileUtils.rm_rf(PACKAGE_DIR)
|
||||
FileUtils.rm_rf(PACKAGE_SRC_DIR)
|
||||
|
||||
# Create the install directory with the old app
|
||||
Dir.mkdir(INSTALL_DIR)
|
||||
copy_file OLDAPP_NAME, "#{INSTALL_DIR}/#{APP_NAME}"
|
||||
|
||||
# Create a dummy file to uninstall
|
||||
uninstall_test_file = create_test_file("#{INSTALL_DIR}/file-to-uninstall.txt", "this file should be removed after the update")
|
||||
uninstall_test_symlink = if not IS_WINDOWS
|
||||
FileUtils.ln_s("#{INSTALL_DIR}/file-to-uninstall.txt", "#{INSTALL_DIR}/symlink-to-file-to-uninstall.txt")
|
||||
else
|
||||
create_test_file("#{INSTALL_DIR}/symlink-to-file-to-uninstall.txt", "dummy file. this is a symlink on Unix")
|
||||
end
|
||||
|
||||
# Populate package source dir with files to install
|
||||
Dir.mkdir(PACKAGE_SRC_DIR)
|
||||
nested_dir_path = "#{PACKAGE_SRC_DIR}/new-dir/new-dir2"
|
||||
FileUtils.mkdir_p(nested_dir_path)
|
||||
FileUtils::chmod 0755, "#{PACKAGE_SRC_DIR}/new-dir"
|
||||
FileUtils::chmod 0755, "#{PACKAGE_SRC_DIR}/new-dir/new-dir2"
|
||||
nested_dir_test_file = "#{nested_dir_path}/new-file.txt"
|
||||
File.open(nested_dir_test_file,'w') do |file|
|
||||
file.puts "this is a new file in a new nested dir"
|
||||
end
|
||||
FileUtils::chmod 0644, nested_dir_test_file
|
||||
copy_file NEWAPP_NAME, "#{PACKAGE_SRC_DIR}/#{APP_NAME}"
|
||||
FileUtils::chmod 0755, "#{PACKAGE_SRC_DIR}/#{APP_NAME}"
|
||||
|
||||
# Create .zip packages from source files
|
||||
Dir.mkdir(PACKAGE_DIR)
|
||||
Dir.chdir(PACKAGE_SRC_DIR) do
|
||||
if !system("#{ZIP_TOOL} #{PACKAGE_DIR}/app-pkg.zip .")
|
||||
raise "Unable to create update package"
|
||||
end
|
||||
end
|
||||
|
||||
# Copy the install script and updater to the target
|
||||
# directory
|
||||
replace_vars("file_list.xml","#{PACKAGE_DIR}/file_list.xml",file_list_vars)
|
||||
copy_file "../#{UPDATER_NAME}", "#{PACKAGE_DIR}/#{UPDATER_NAME}"
|
||||
|
||||
# Run the updater using the new syntax
|
||||
#
|
||||
# Run the application from the install directory to
|
||||
# make sure that it looks in the correct directory for
|
||||
# the file_list.xml file and packages
|
||||
#
|
||||
install_path = File.expand_path(INSTALL_DIR)
|
||||
Dir.chdir(INSTALL_DIR) do
|
||||
flags = "--force-elevated" if force_elevation
|
||||
debug_flags = "gdb --args" if run_in_debugger
|
||||
cmd = "#{debug_flags} #{PACKAGE_DIR}/#{UPDATER_NAME} #{flags} --install-dir \"#{install_path}\" --package-dir \"#{PACKAGE_DIR}\" --script file_list.xml --auto-close"
|
||||
puts "Running '#{cmd}'"
|
||||
system(cmd)
|
||||
end
|
||||
|
||||
# TODO - Correctly wait until updater has finished
|
||||
sleep(1)
|
||||
|
||||
# Check that the app was updated
|
||||
app_path = "#{INSTALL_DIR}/#{APP_NAME}"
|
||||
output = `"#{app_path}"`
|
||||
if (output.strip != "new app starting")
|
||||
throw "Updated app produced unexpected output: #{output}"
|
||||
end
|
||||
|
||||
# Check that the packaged dir and install dir match
|
||||
dir_diff = compare_dirs(PACKAGE_SRC_DIR, INSTALL_DIR)
|
||||
ignored_files = ["test-dir", "test-dir/app-symlink", UPDATER_NAME]
|
||||
have_unexpected_change = false
|
||||
dir_diff.each do |path, change_type|
|
||||
if !ignored_files.include?(path)
|
||||
case change_type
|
||||
when :added
|
||||
$stderr.puts "File #{path} was not installed"
|
||||
when :changed
|
||||
$stderr.puts "File #{path} differs between install and package dir"
|
||||
when :deleted
|
||||
$stderr.puts "File #{path} was not uninstalled"
|
||||
end
|
||||
have_unexpected_change = true
|
||||
end
|
||||
end
|
||||
|
||||
if have_unexpected_change
|
||||
throw "Unexpected differences between packaging and update dir"
|
||||
end
|
||||
|
||||
puts "Test passed"
|
@ -1,67 +0,0 @@
|
||||
<?xml version="1.0"?>
|
||||
|
||||
<!-- The v2-compatible attribute lets the update script parser
|
||||
know that it is dealing with a script structured for backwards
|
||||
compatibility with the MD <= 1.0 updater.
|
||||
!-->
|
||||
<update version="3" v2-compatible="true">
|
||||
<targetVersion>2.0</targetVersion>
|
||||
<platform>Test</platform>
|
||||
<dependencies>
|
||||
<!-- The new updater is standalone and has no dependencies,
|
||||
except for standard system libraries and itself.
|
||||
!-->
|
||||
</dependencies>
|
||||
<packages>
|
||||
<package>
|
||||
<name>app-pkg</name>
|
||||
<hash>$APP_PACKAGE_HASH</hash>
|
||||
<size>$APP_PACKAGE_SIZE</size>
|
||||
<source>http://some/dummy/URL</source>
|
||||
</package>
|
||||
</packages>
|
||||
|
||||
<!-- For compatibility with the update download in MD <= 1.0,
|
||||
an <install> section lists the packages to download and
|
||||
the real list of files to install is in the <install-v3>
|
||||
section. !-->
|
||||
<install>
|
||||
<!-- A duplicate of the <packages> section should appear here,
|
||||
except that each package is listed using the same structure
|
||||
as files in the install-v3/files section.
|
||||
!-->
|
||||
</install>
|
||||
<install-v3>
|
||||
<file>
|
||||
<name>$APP_FILENAME</name>
|
||||
<hash>$UPDATED_APP_HASH</hash>
|
||||
<size>$UPDATED_APP_SIZE</size>
|
||||
<permissions>0755</permissions>
|
||||
<package>app-pkg</package>
|
||||
<is-main-binary>true</is-main-binary>
|
||||
</file>
|
||||
<file>
|
||||
<name>$UPDATER_FILENAME</name>
|
||||
<hash>$UPDATER_HASH</hash>
|
||||
<size>$UPDATER_SIZE</size>
|
||||
<permissions>0755</permissions>
|
||||
</file>
|
||||
<!-- Test symlink !-->
|
||||
<file>
|
||||
<name>test-dir/app-symlink</name>
|
||||
<target>../app</target>
|
||||
</file>
|
||||
<file>
|
||||
<name>new-dir/new-dir2/new-file.txt</name>
|
||||
<hash>$TEST_FILENAME</hash>
|
||||
<size>$TEST_SIZE</size>
|
||||
<package>app-pkg</package>
|
||||
<permissions>0644</permissions>
|
||||
</file>
|
||||
</install-v3>
|
||||
<uninstall>
|
||||
<!-- TODO - List some files to uninstall here !-->
|
||||
<file>file-to-uninstall.txt</file>
|
||||
<file>symlink-to-file-to-uninstall.txt</file>
|
||||
</uninstall>
|
||||
</update>
|
Loading…
Reference in New Issue
Block a user