#include "GoUpdate.h" #include #include #include #include #include namespace GoUpdate { bool parseVersionInfo(const QByteArray &data, VersionFileList &list, QString &error) { QJsonParseError jsonError; QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError); if (jsonError.error != QJsonParseError::NoError) { error = QString("Failed to parse version info JSON: %1 at %2") .arg(jsonError.errorString()) .arg(jsonError.offset); qCritical() << error; return false; } QJsonObject json = jsonDoc.object(); qDebug() << data; qDebug() << "Loading version info from JSON."; QJsonArray filesArray = json.value("Files").toArray(); for (QJsonValue fileValue : filesArray) { QJsonObject fileObj = fileValue.toObject(); QString file_path = fileObj.value("Path").toString(); #ifdef Q_OS_MAC // On OSX, the paths for the updater need to be fixed. // basically, anything that isn't in the .app folder is ignored. // everything else is changed so the code that processes the files actually finds // them and puts the replacements in the right spots. if (!fixPathForOSX(file_path)) continue; #endif VersionFileEntry file{file_path, fileObj.value("Perms").toVariant().toInt(), FileSourceList(), fileObj.value("MD5").toString(), }; qDebug() << "File" << file.path << "with perms" << file.mode; QJsonArray sourceArray = fileObj.value("Sources").toArray(); for (QJsonValue val : sourceArray) { QJsonObject sourceObj = val.toObject(); QString type = sourceObj.value("SourceType").toString(); if (type == "http") { file.sources.append(FileSource("http", sourceObj.value("Url").toString())); } else { qWarning() << "Unknown source type" << type << "ignored."; } } qDebug() << "Loaded info for" << file.path; list.append(file); } return true; } bool processFileLists ( const VersionFileList ¤tVersion, const VersionFileList &newVersion, const QString &rootPath, const QString &tempPath, NetJobPtr job, OperationList &ops, bool useLocalUpdater ) { // First, if we've loaded the current version's file list, we need to iterate through it and // delete anything in the current one version's list that isn't in the new version's list. for (VersionFileEntry entry : currentVersion) { QFileInfo toDelete(PathCombine(rootPath, entry.path)); if (!toDelete.exists()) { qCritical() << "Expected file " << toDelete.absoluteFilePath() << " doesn't exist!"; } bool keep = false; // for (VersionFileEntry newEntry : newVersion) { if (newEntry.path == entry.path) { qDebug() << "Not deleting" << entry.path << "because it is still present in the new version."; keep = true; break; } } // If the loop reaches the end and we didn't find a match, delete the file. if (!keep) { if (toDelete.exists()) ops.append(Operation::DeleteOp(entry.path)); } } // Next, check each file in MultiMC's folder and see if we need to update them. for (VersionFileEntry entry : newVersion) { // TODO: Let's not MD5sum a ton of files on the GUI thread. We should probably find a // way to do this in the background. QString fileMD5; QString realEntryPath = PathCombine(rootPath, entry.path); QFile entryFile(realEntryPath); QFileInfo entryInfo(realEntryPath); bool needs_upgrade = false; if (!entryFile.exists()) { needs_upgrade = true; } else { bool pass = true; if (!entryInfo.isReadable()) { qCritical() << "File " << realEntryPath << " is not readable."; pass = false; } if (!entryInfo.isWritable()) { qCritical() << "File " << realEntryPath << " is not writable."; pass = false; } if (!entryFile.open(QFile::ReadOnly)) { qCritical() << "File " << realEntryPath << " cannot be opened for reading."; pass = false; } if (!pass) { ops.clear(); return false; } } if(!needs_upgrade) { QCryptographicHash hash(QCryptographicHash::Md5); auto foo = entryFile.readAll(); hash.addData(foo); fileMD5 = hash.result().toHex(); if ((fileMD5 != entry.md5)) { qDebug() << "MD5Sum does not match!"; qDebug() << "Expected:'" << entry.md5 << "'"; qDebug() << "Got: '" << fileMD5 << "'"; needs_upgrade = true; } } // skip file. it doesn't need an upgrade. if (!needs_upgrade) { qDebug() << "File" << realEntryPath << " does not need updating."; continue; } // yep. this file actually needs an upgrade. PROCEED. qDebug() << "Found file" << realEntryPath << " that needs updating."; // if it's the updater we want to treat it separately bool isUpdater = entry.path.endsWith("updater") || entry.path.endsWith("updater.exe"); // Go through the sources list and find one to use. // TODO: Make a NetAction that takes a source list and tries each of them until one // works. For now, we'll just use the first http one. for (FileSource source : entry.sources) { if (source.type != "http") continue; qDebug() << "Will download" << entry.path << "from" << source.url; // Download it to updatedir/- where filepath is the file's // path with slashes replaced by underscores. QString dlPath = PathCombine(tempPath, QString(entry.path).replace("/", "_")); if (isUpdater) { if(useLocalUpdater) { qDebug() << "Skipping updater download and using local version."; } else { auto cache_entry = ENV.metacache()->resolveEntry("root", entry.path); qDebug() << "Updater will be in " << cache_entry->getFullPath(); // force check. cache_entry->stale = true; auto download = CacheDownload::make(QUrl(source.url), cache_entry); job->addNetAction(download); } } else { // We need to download the file to the updatefiles folder and add a task // to copy it to its install path. auto download = MD5EtagDownload::make(source.url, dlPath); download->m_expected_md5 = entry.md5; job->addNetAction(download); ops.append(Operation::CopyOp(dlPath, entry.path, entry.mode)); } } } return true; } bool fixPathForOSX(QString &path) { if (path.startsWith("MultiMC.app/")) { // remove the prefix and add a new, more appropriate one. path.remove(0, 12); return true; } else { qCritical() << "Update path not within .app: " << path; return false; } } bool writeInstallScript(OperationList &opsList, QString scriptFile) { // Build the base structure of the XML document. QDomDocument doc; QDomElement root = doc.createElement("update"); root.setAttribute("version", "3"); doc.appendChild(root); QDomElement installFiles = doc.createElement("install"); root.appendChild(installFiles); QDomElement removeFiles = doc.createElement("uninstall"); root.appendChild(removeFiles); // Write the operation list to the XML document. for (Operation op : opsList) { QDomElement file = doc.createElement("file"); switch (op.type) { case Operation::OP_COPY: { // Install the file. QDomElement name = doc.createElement("source"); QDomElement path = doc.createElement("dest"); QDomElement mode = doc.createElement("mode"); name.appendChild(doc.createTextNode(op.file)); path.appendChild(doc.createTextNode(op.dest)); // We need to add a 0 at the beginning here, because Qt doesn't convert to octal // correctly. mode.appendChild(doc.createTextNode("0" + QString::number(op.mode, 8))); file.appendChild(name); file.appendChild(path); file.appendChild(mode); installFiles.appendChild(file); qDebug() << "Will install file " << op.file << " to " << op.dest; } break; case Operation::OP_DELETE: { // Delete the file. file.appendChild(doc.createTextNode(op.file)); removeFiles.appendChild(file); qDebug() << "Will remove file" << op.file; } break; default: qWarning() << "Can't write update operation of type" << op.type << "to file. Not implemented."; continue; } } // Write the XML document to the file. QFile outFile(scriptFile); if (outFile.open(QIODevice::WriteOnly)) { outFile.write(doc.toByteArray()); } else { return false; } return true; } }