diff --git a/src/downloadextractthread.cpp b/src/downloadextractthread.cpp index a968ddecc..72a048348 100644 --- a/src/downloadextractthread.cpp +++ b/src/downloadextractthread.cpp @@ -19,6 +19,7 @@ #include #include #include +#include using namespace std; @@ -355,83 +356,11 @@ void DownloadExtractThread::extractMultiFileRun() } if(_initFormat == "UNRAID") { - if(_allNetworkSettingsPresent() && _imgWriterSettings["static"].toBool()) - { - QFile fileNetwork(folder + "/config/network.cfg"); - if (fileNetwork.exists()) - { - fileNetwork.open(QIODevice::ReadOnly); - QString dataText = fileNetwork.readAll(); - fileNetwork.close(); - - dataText.replace("USE_DHCP=\"yes\"", "USE_DHCP=\"no\""); - dataText.replace("IPADDR=", "IPADDR=\"" + _imgWriterSettings["ipaddr"].toString() + "\""); - dataText.replace("NETMASK=", "NETMASK=\"" + _imgWriterSettings["netmask"].toString() + "\""); - dataText.replace("GATEWAY=", "GATEWAY=\"" + _imgWriterSettings["gateway"].toString() + "\""); - dataText.append("DNS_SERVER1=\"" + _imgWriterSettings["dns"].toString() + "\"\r\n"); - - if (fileNetwork.open(QFile::WriteOnly | QFile::Truncate)) - { - QTextStream out(&fileNetwork); - out << dataText; - } - fileNetwork.close(); - } - - } - if(_imgWriterSettings.contains("servername")) { - QFile fileIdent(folder + "/config/ident.cfg"); - if (fileIdent.exists()) - { - fileIdent.open(QIODevice::ReadOnly); - QString dataText = fileIdent.readAll(); - fileIdent.close(); - - dataText.replace("NAME=\"Tower\"", "NAME=\"" + _imgWriterSettings["servername"].toString() + "\""); - - if (fileIdent.open(QFile::WriteOnly | QFile::Truncate)) - { - QTextStream out(&fileIdent); - out << dataText; - } - fileIdent.close(); - } - } - - // restore make bootable scripts and/or syslinux, if necessary - QDir dirTarget(folder); - if (dirTarget.mkdir("syslinux")) - { - QFile::copy(":/unraid/syslinux/ldlinux.c32", folder + "/syslinux/ldlinux.c32"); - QFile::copy(":/unraid/syslinux/libcom32.c32", folder + "/syslinux/libcom32.c32"); - QFile::copy(":/unraid/syslinux/libutil.c32", folder + "/syslinux/libutil.c32"); - QFile::copy(":/unraid/syslinux/make_bootable_linux.sh", folder + "/syslinux/make_bootable_linux.sh"); - QFile::copy(":/unraid/syslinux/make_bootable_mac.sh", folder + "/syslinux/make_bootable_mac.sh"); - QFile::copy(":/unraid/syslinux/mboot.c32", folder + "/syslinux/mboot.c32"); - QFile::copy(":/unraid/syslinux/mbr.bin", folder + "/syslinux/mbr.bin"); - QFile::copy(":/unraid/syslinux/menu.c32", folder + "/syslinux/menu.c32"); - QFile::copy(":/unraid/syslinux/syslinux", folder + "/syslinux/syslinux"); - QFile::copy(":/unraid/syslinux/syslinux_linux", folder + "/syslinux/syslinux_linux"); - QFile::copy(":/unraid/syslinux/syslinux.cfg", folder + "/syslinux/syslinux.cfg"); - QFile::copy(":/unraid/syslinux/syslinux.cfg-", folder + "/syslinux/syslinux.cfg-"); - QFile::copy(":/unraid/syslinux/syslinux.exe", folder + "/syslinux/syslinux.exe"); - QFile::copy(":/unraid/make_bootable_linux", folder + "/make_bootable_linux"); - QFile::copy(":/unraid/make_bootable_mac", folder + "/make_bootable_mac"); - QFile::copy(":/unraid/make_bootable.bat", folder + "/make_bootable.bat"); - } - -#ifdef Q_OS_WIN - QString program{"cmd.exe"}; - QStringList args; - args << "/C" << "echo Y | make_bootable.bat"; - - int retcode = QProcess::execute(program, args); - - if (retcode) - { - throw runtime_error("Error running make_bootable script"); - } -#endif + _writeUnraidNetworkSettings(); + _writeUnraidServerSettings(); + _writeUnraidLanguageSettings(); + _writeUnraidSyslinuxFiles(); + _runUnraidMakeBootableScript(); } emit success(); } @@ -553,3 +482,182 @@ void DownloadExtractThread::_pushQueue(const char *data, size_t len) _cv.notify_one(); } } + +void DownloadExtractThread::_writeUnraidNetworkSettings(const QString& dstDir) +{ + if(_allNetworkSettingsPresent() && _imgWriterSettings["static"].toBool()) + { + QFile fileNetwork(dstDir + "/config/network.cfg"); + if (fileNetwork.exists()) + { + fileNetwork.open(QIODevice::ReadOnly); + QString dataText = fileNetwork.readAll(); + fileNetwork.close(); + + dataText.replace("USE_DHCP=\"yes\"", "USE_DHCP=\"no\""); + dataText.replace("IPADDR=", "IPADDR=\"" + _imgWriterSettings["ipaddr"].toString() + "\""); + dataText.replace("NETMASK=", "NETMASK=\"" + _imgWriterSettings["netmask"].toString() + "\""); + dataText.replace("GATEWAY=", "GATEWAY=\"" + _imgWriterSettings["gateway"].toString() + "\""); + dataText.append("DNS_SERVER1=\"" + _imgWriterSettings["dns"].toString() + "\"\r\n"); + + if (fileNetwork.open(QFile::WriteOnly | QFile::Truncate)) + { + QTextStream out(&fileNetwork); + out << dataText; + } + fileNetwork.close(); + } + + } +} + +void DownloadExtractThread::_writeUnraidServerSettings(const QString& dstDir) +{ + if(_imgWriterSettings.contains("servername")) { + QFile fileIdent(dstDir + "/config/ident.cfg"); + if (fileIdent.exists()) + { + fileIdent.open(QIODevice::ReadOnly); + QString dataText = fileIdent.readAll(); + fileIdent.close(); + + dataText.replace("NAME=\"Tower\"", "NAME=\"" + _imgWriterSettings["servername"].toString() + "\""); + + if (fileIdent.open(QFile::WriteOnly | QFile::Truncate)) + { + QTextStream out(&fileIdent); + out << dataText; + } + fileIdent.close(); + } + } +} + +void DownloadExtractThread::_writeUnraidLanguageSettings(const QString& dstDir) +{ + if(_imgWriterSettings.contains("UNRAID_LANG_JSON")) { + // remove this tmp file now that we're definitely done with it + QFile langJson(_imgWriterSettings["UNRAID_LANG_JSON"].toByteArray()); + langJson.remove(); + } + + if(_imgWriterSettings.contains("UNRAID_LANG_CODE")) + { + QString unraidLangCode(_imgWriterSettings["UNRAID_LANG_CODE"].toString()); + if(_imgWriterSettings.contains("UNRAID_LANG_XML")) { + QFile langXml(_imgWriterSettings["UNRAID_LANG_XML"].toByteArray()); + QFile::rename(langXml.fileName(), dstDir + "/config/plugins/lang-" + unraidLangCode + ".xml"); + } + + if(_imgWriterSettings.contains("UNRAID_LANG_ZIP")) { + QFile langZip(_imgWriterSettings["UNRAID_LANG_ZIP"].toByteArray()); + QFile::rename(langZip.fileName(), dstDir + "/config/plugins/dynamix/lang-" + unraidLangCode + ".zip"); + } + QFile dynamixCfg(dstDir + "/config/plugins/dynamix/dynamix.cfg"); + if (dynamixCfg.exists()) + { + dynamixCfg.open(QIODevice::ReadOnly); + QString dataText = dynamixCfg.readAll(); + dynamixCfg.close(); + + _addLocaleToUnraidDynamixConfig(dataText, unraidLangCode); + + if (dynamixCfg.open(QFile::WriteOnly | QFile::Truncate)) + { + QTextStream out(&dynamixCfg); + out << dataText; + } + dynamixCfg.close(); + } + } +} + +void DownloadExtractThread::_writeUnraidSyslinuxFiles(const QString& dstDir) +{ + // restore make bootable scripts and/or syslinux, if necessary + QDir dirTarget(dstDir); + if (dirTarget.mkdir("syslinux")) + { + QFile::copy(":/unraid/syslinux/ldlinux.c32", dstDir + "/syslinux/ldlinux.c32"); + QFile::copy(":/unraid/syslinux/libcom32.c32", dstDir + "/syslinux/libcom32.c32"); + QFile::copy(":/unraid/syslinux/libutil.c32", dstDir + "/syslinux/libutil.c32"); + QFile::copy(":/unraid/syslinux/make_bootable_linux.sh", dstDir + "/syslinux/make_bootable_linux.sh"); + QFile::copy(":/unraid/syslinux/make_bootable_mac.sh", dstDir + "/syslinux/make_bootable_mac.sh"); + QFile::copy(":/unraid/syslinux/mboot.c32", dstDir + "/syslinux/mboot.c32"); + QFile::copy(":/unraid/syslinux/mbr.bin", dstDir + "/syslinux/mbr.bin"); + QFile::copy(":/unraid/syslinux/menu.c32", dstDir + "/syslinux/menu.c32"); + QFile::copy(":/unraid/syslinux/syslinux", dstDir + "/syslinux/syslinux"); + QFile::copy(":/unraid/syslinux/syslinux_linux", dstDir + "/syslinux/syslinux_linux"); + QFile::copy(":/unraid/syslinux/syslinux.cfg", dstDir + "/syslinux/syslinux.cfg"); + QFile::copy(":/unraid/syslinux/syslinux.cfg-", dstDir + "/syslinux/syslinux.cfg-"); + QFile::copy(":/unraid/syslinux/syslinux.exe", dstDir + "/syslinux/syslinux.exe"); + QFile::copy(":/unraid/make_bootable_linux", dstDir + "/make_bootable_linux"); + QFile::copy(":/unraid/make_bootable_mac", dstDir + "/make_bootable_mac"); + QFile::copy(":/unraid/make_bootable.bat", dstDir + "/make_bootable.bat"); + } +} + +void DownloadExtractThread::_runUnraidMakeBootableScript() +{ +#ifdef Q_OS_WIN + QString program{"cmd.exe"}; + QStringList args; + args << "/C" << "echo Y | make_bootable.bat"; + + int retcode = QProcess::execute(program, args); + + if (retcode) + { + throw runtime_error("Error running make_bootable script"); + } +#endif +} + +void DownloadExtractThread::_addLocaleToUnraidDynamixConfig(QString& dataText, const QString& unraidLangCode) +{ + QDomDocument doc; + QString errorMsg; + int errorLine, errorColumn; + + // Load the dataText as an XML document + if (!doc.setContent(dataText, &errorMsg, &errorLine, &errorColumn)) + { + throw runtime_error("Error parsing XML at line" + errorLine + "column" + errorColumn + ":" + errorMsg); + } + + // Find the section + QDomElement root = doc.documentElement(); // Get the root element + QDomNodeList displaySections = root.elementsByTagName("display"); + + if (!displaySections.isEmpty()) + { + QDomElement display = displaySections.at(0).toElement(); // Assuming only one section + + if (!display.isNull()) + { + // Check if "locale" attribute exists + if (display.hasAttribute("locale")) + { + display.setAttribute("locale", unraidLangCode); + } + else + { + // Add "locale" attribute if not present + display.setAttribute("locale", unraidLangCode); + } + } + } + else + { + // Add a new [display] section with the locale attribute + QDomElement newDisplay = doc.createElement("display"); + newDisplay.setAttribute("locale", unraidLangCode); + root.appendChild(newDisplay); + } + + // Convert the modified XML back to a string + QString newXml; + QTextStream stream(&newXml); + doc.save(stream, 4); // Indent with 4 spaces + dataText = newXml; +} diff --git a/src/downloadextractthread.h b/src/downloadextractthread.h index 002b9bf94..cb0dae80c 100644 --- a/src/downloadextractthread.h +++ b/src/downloadextractthread.h @@ -58,6 +58,13 @@ class DownloadExtractThread : public DownloadThread static ssize_t _archive_read(struct archive *a, void *client_data, const void **buff); static int _archive_close(struct archive *a, void *client_data); + + void _writeUnraidNetworkSettings(const QString& dstDir); + void _writeUnraidServerSettings(const QString& dstDir); + void _writeUnraidLanguageSettings(const QString& dstDir); + void _writeUnraidSyslinuxFiles(const QString& dstDir); + void _runUnraidMakeBootableScript(); + void _addLocaleToUnraidDynamixConfig(QString& dataText, const QString& unraidLangCode); }; #endif // DOWNLOADEXTRACTTHREAD_H diff --git a/src/imagewriter.cpp b/src/imagewriter.cpp index 5281b1f42..8c97addaf 100644 --- a/src/imagewriter.cpp +++ b/src/imagewriter.cpp @@ -34,6 +34,7 @@ #include #include #include +#include #ifndef QT_NO_WIDGETS #include #include @@ -188,6 +189,7 @@ ImageWriter::ImageWriter(QObject *parent) { _currentLang = langname; _currentLangcode = currentlangcode; + _unraidLangcode = _generateUnraidLangcode(currentlangcode, langname); } } //_currentKeyboard = "us"; @@ -354,15 +356,24 @@ void ImageWriter::startWrite() if (_multipleFilesInZip) { static_cast(_thread)->enableMultipleFileExtraction(); - QString label{""}; if(_initFormat == "UNRAID") { - // if this is an unraid zip, the volume label needs to be UNRAID for the make bootable script to work as intended - label = "UNRAID"; + // in order to properly propagate language selection to the unraid usb files themselves, + // we need to kick of a chain of events that starts with formatting the drive, + // which then kicks off a download of the available languages json, + // which then kicks off a download of the select language xml, + // which, if there is a zip file specified in that xml, then kicks off a download of that zip file, + // which then kicks of the download/extract of the actual unraid files + QString label{"UNRAID"}; // since this is an unraid zip, the volume label needs to be UNRAID for the make bootable script to work as intended + DriveFormatThread *dft = new DriveFormatThread(_dst.toLatin1(), label, this); + connect(dft, SIGNAL(error(QString)), SLOT(onError(QString))); + connect(dft, SIGNAL(success()), SLOT(onUnraidFormatComplete())); + dft->start(); + } else { + DriveFormatThread *dft = new DriveFormatThread(_dst.toLatin1(), "", this); + connect(dft, SIGNAL(success()), _thread, SLOT(start())); + connect(dft, SIGNAL(error(QString)), SLOT(onError(QString))); + dft->start(); } - DriveFormatThread *dft = new DriveFormatThread(_dst.toLatin1(), label, this); - connect(dft, SIGNAL(success()), _thread, SLOT(start())); - connect(dft, SIGNAL(error(QString)), SLOT(onError(QString))); - dft->start(); } else { @@ -1355,6 +1366,7 @@ void ImageWriter::changeLanguage(const QString &newLanguageName) replaceTranslator(trans); _currentLang = newLanguageName; _currentLangcode = langcode; + _unraidLangcode = _generateUnraidLangcode(_currentLangcode, _currentLang); } else { @@ -1520,3 +1532,160 @@ bool ImageWriter::windowsBuild() { return false; #endif } + +QString ImageWriter::_generateUnraidLangcode(const QString& langcode, const QString& languageName) +{ + QString unraidLangcode = langcode + "_"; + + if (langcode == "en") { + unraidLangcode.append("US"); + } + else if (langcode == "pt") { + if (languageName.contains("Brasil")) { + unraidLangcode.append("BR"); + } + else { + unraidLangcode.append("PT"); + } + } + else if (langcode == "sv") { + unraidLangcode.append("SE"); + } + else if (langcode == "uk") { + unraidLangcode.append("UA"); + } + else if (langcode == "zh") { + unraidLangcode.append("CN"); + } + else { + unraidLangcode.append(langcode.toUpper()); + } + return unraidLangcode; +} + +QByteArray ImageWriter::_parseUnraidLangJson(const QByteArray& jsonFilePath, const QString& unraidLangCode) +{ + QByteArray jsonData = _readFileContents(jsonFilePath); + QJsonParseError parseError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData, &parseError); + + if (parseError.error != QJsonParseError::NoError) + { + onError(parseError.errorString()); + } + + QJsonObject jsonObj = jsonDoc.object(); + + QJsonValue langVal = jsonObj.value(unraidLangCode); + QJsonObject langObj = langVal.toObject(); + + QJsonValue url = langObj.value("URL"); + return url.toString().toUtf8(); +} + +QString ImageWriter::_parseUnraidLangXml(const QByteArray& xmlFilePath) +{ + QByteArray xmlData = _readFileContents(xmlFilePath); + QXmlStreamReader xml(xmlData); + QString found; + + while (!xml.atEnd() && !xml.hasError()) + { + QXmlStreamReader::TokenType token = xml.readNext(); + if (token != QXmlStreamReader::StartElement) + { + continue; + } + if (xml.name().compare("LanguageURL") != 0) + { + continue; + } + xml.readNext(); + if (xml.atEnd()) + { + break; + } + if (xml.isCharacters() && (xml.text().compare("\n") != 0)) + { + found.append(xml.text().toString()); + } + } + + return found; +} + +QByteArray ImageWriter::_readFileContents(const QByteArray& filePath) +{ + QFile fileIn(filePath); + if(!fileIn.open(QIODevice::ReadOnly)) + { + onError("Error opening " + filePath); + } + // remove excess null characters (they cause the qt json parser to fail) + QByteArray data = fileIn.readAll().replace('\0', ""); + fileIn.close(); + if(!fileIn.open(QIODevice::WriteOnly)) + { + onError("Error opening " + filePath); + } + fileIn.write(data); + return data; +} + +void ImageWriter::onUnraidFormatComplete() +{ + _unraidLangJsonTmpPath = _generateTempFilePath(); + setSetting("imagecustomization/UNRAID_LANG_JSON", _unraidLangJsonTmpPath); + DownloadThread * dl_thread = new DownloadThread(_unraidLangJsonUrl, _unraidLangJsonTmpPath, "", this); + connect(dl_thread, SIGNAL(error(QString)), SLOT(onError(QString))); + connect(dl_thread, SIGNAL(success()), SLOT(onUnraidJsonDownloadComplete())); + dl_thread->start(); +} + +void ImageWriter::onUnraidJsonDownloadComplete() +{ + QByteArray xmlUrl = _parseUnraidLangJson(_unraidLangJsonTmpPath, _unraidLangcode); + _unraidLangXmlTmpPath = _generateTempFilePath(); + setSetting("imagecustomization/UNRAID_LANG_XML", _unraidLangXmlTmpPath); + DownloadThread * dl_thread = new DownloadThread(xmlUrl, _unraidLangXmlTmpPath, "", this); + connect(dl_thread, SIGNAL(error(QString)), SLOT(onError(QString))); + connect(dl_thread, SIGNAL(success()), SLOT(onUnraidXmlDownloadComplete())); + dl_thread->start(); +} + +void ImageWriter::onUnraidXmlDownloadComplete() +{ + QString zipUrl = _parseUnraidLangXml(_unraidLangXmlTmpPath); + setSetting("imagecustomization/UNRAID_LANG_CODE", _unraidLangcode); + if(zipUrl.isEmpty()) + { + // this has to be called again so getSavedCustomizationSettings returns the updated qvariantmap + _thread->setImageCustomization(_config, _cmdline, _firstrun, _cloudinit, _cloudinitNetwork, _initFormat, getSavedCustomizationSettings()); + _thread->start(); + } + else + { + _unraidLangZipTmpPath = _generateTempFilePath(); + setSetting("imagecustomization/UNRAID_LANG_ZIP", _unraidLangZipTmpPath); + // this has to be called again so getSavedCustomizationSettings returns the updated qvariantmap + _thread->setImageCustomization(_config, _cmdline, _firstrun, _cloudinit, _cloudinitNetwork, _initFormat, getSavedCustomizationSettings()); + DownloadThread * dl_thread = new DownloadThread(zipUrl.toUtf8(), _unraidLangZipTmpPath, "", this); + connect(dl_thread, SIGNAL(error(QString)), SLOT(onError(QString))); + connect(dl_thread, SIGNAL(success()), _thread, SLOT(start())); + dl_thread->start(); + } +} + +QByteArray ImageWriter::_generateTempFilePath(bool create) +{ + QString uuid = QUuid::createUuid().toString(QUuid::WithoutBraces); + QString downloadDir = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation); + QByteArray tmpFilePath = (downloadDir + "/unraid-usb-creator-" + uuid + ".tmp").toUtf8(); + if(create) + { + QFile tmpFile(tmpFilePath); + tmpFile.open(QIODevice::ReadWrite); + } + return tmpFilePath; +} + diff --git a/src/imagewriter.h b/src/imagewriter.h index c2e5c5c00..466ddc5b4 100644 --- a/src/imagewriter.h +++ b/src/imagewriter.h @@ -16,6 +16,7 @@ #include #include #include +#include #include "config.h" #include "powersaveblocker.h" #include "drivelistmodel.h" @@ -183,6 +184,9 @@ protected slots: void onPreparationStatusUpdate(QString msg); void handleNetworkRequestFinished(QNetworkReply *data); void onSTPdetected(); + void onUnraidFormatComplete(); + void onUnraidJsonDownloadComplete(); + void onUnraidXmlDownloadComplete(); private: // Recursively walk all the entries with subitems and, for any which @@ -195,7 +199,7 @@ protected slots: protected: QUrl _src, _repo; - QString _dst, _cacheFileName, _parentCategory, _osName, _currentLang, _currentLangcode, _currentKeyboard; + QString _dst, _cacheFileName, _parentCategory, _osName, _currentLang, _currentLangcode, _currentKeyboard, _unraidLangcode; QByteArray _expectedHash, _cachedFileHash, _cmdline, _config, _firstrun, _cloudinit, _cloudinitNetwork, _initFormat; quint64 _downloadLen, _extrLen, _devLen, _dlnow, _verifynow; DriveListModel _drivelist; @@ -212,6 +216,10 @@ protected slots: QWinTaskbarButton *_taskbarButton; #endif bool _guidValid; + const QByteArray _unraidLangJsonUrl{"https://assets.ca.unraid.net/feed/languageSelection.json"}; + QByteArray _unraidLangJsonTmpPath; + QByteArray _unraidLangXmlTmpPath; + QByteArray _unraidLangZipTmpPath; void _parseCompressedFile(); void _parseXZFile(); @@ -219,6 +227,11 @@ protected slots: QString _privKeyFileName(); QString _sshKeyDir(); QString _sshKeyGen(); + QString _generateUnraidLangcode(const QString& langcode, const QString& languageName); + QByteArray _readFileContents(const QByteArray& filePath); + QString _parseUnraidLangXml(const QByteArray& xmlFilePath); + QByteArray _parseUnraidLangJson(const QByteArray& jsonFilePath, const QString& unraidLangCode); + QByteArray _generateTempFilePath(bool create = true); }; #endif // IMAGEWRITER_H diff --git a/src/qmlcomponents/regex_validator_qt6.qml b/src/qmlcomponents/regex_validator_qt6.qml index db2c280fd..d3830c9db 100644 --- a/src/qmlcomponents/regex_validator_qt6.qml +++ b/src/qmlcomponents/regex_validator_qt6.qml @@ -1,4 +1,4 @@ -import QtQuick +import QtQuick 2.9 RegularExpressionValidator { regularExpression: /^[A-Za-z0-9]([A-Za-z0-9\-\.]{0,13}[A-Za-z0-9])?$/