/* * SPDX-License-Identifier: Apache-2.0 * Copyright (C) 2020 Raspberry Pi (Trading) Limited */ #include "imagewriter.h" #include "drivelistitem.h" #include "downloadextractthread.h" #include "dependencies/drivelist/src/drivelist.hpp" #include "driveformatthread.h" #include #include #include #include #include #include #include #include #include #include #include #ifdef Q_OS_WIN #include #include #endif ImageWriter::ImageWriter(QObject *parent) : QObject(parent), _repo(QUrl(QString(OSLIST_URL))), _dlnow(0), _verifynow(0), _engine(nullptr), _thread(nullptr), _verifyEnabled(false), _cachingEnabled(false) { connect(&_polltimer, SIGNAL(timeout()), SLOT(pollProgress())); #ifdef Q_OS_WIN QProcess *p = new QProcess(this); p->start("net stop ShellHWDetection"); #endif if (!_settings.isWritable() && !_settings.fileName().isEmpty()) { /* Settings file is not writable, probably run by root previously */ QString settingsFile = _settings.fileName(); qDebug() << "Settings file" << settingsFile << "not writable. Recreating it"; QFile f(_settings.fileName()); QByteArray oldsettings; if (f.open(f.ReadOnly)) { oldsettings = f.readAll(); f.close(); } f.remove(); if (f.open(f.WriteOnly)) { f.write(oldsettings); f.close(); _settings.sync(); } else { qDebug() << "Error deleting and recreating settings file. Please remove manually."; } } _settings.beginGroup("caching"); _cachingEnabled = _settings.value("enabled", IMAGEWRITER_ENABLE_CACHE_DEFAULT).toBool(); _cachedFileHash = _settings.value("lastDownloadSHA256").toByteArray(); _cacheFileName = QStandardPaths::writableLocation(QStandardPaths::CacheLocation)+QDir::separator()+"lastdownload.cache"; if (!_cachedFileHash.isEmpty()) { QFileInfo f(_cacheFileName); if (!f.exists() || !f.isReadable() || !f.size()) { _cachedFileHash.clear(); _settings.remove("lastDownloadSHA256"); _settings.sync(); } } _settings.endGroup(); } ImageWriter::~ImageWriter() { #ifdef Q_OS_WIN QProcess *p = new QProcess(this); p->startDetached("net start ShellHWDetection"); #endif } void ImageWriter::setEngine(QQmlApplicationEngine *engine) { _engine = engine; } /* Set URL to download from */ void ImageWriter::setSrc(const QUrl &url, quint64 downloadLen, quint64 extrLen, QByteArray expectedHash, bool multifilesinzip) { _src = url; _downloadLen = downloadLen; _extrLen = extrLen; _expectedHash = expectedHash; _multipleFilesInZip = multifilesinzip; if (!_downloadLen && url.isLocalFile()) { QFileInfo fi(url.toLocalFile()); _downloadLen = fi.size(); } } /* Set device to write to */ void ImageWriter::setDst(const QString &device, quint64 deviceSize) { _dst = device; _devLen = deviceSize; } /* Returns true if src and dst are set */ bool ImageWriter::readyToWrite() { return !_src.isEmpty() && !_dst.isEmpty(); } /* Start writing */ void ImageWriter::startWrite() { if (!readyToWrite()) return; if (_src.toString() == "internal://format") { DriveFormatThread *dft = new DriveFormatThread(_dst.toLatin1(), this); connect(dft, SIGNAL(success()), SLOT(onSuccess())); connect(dft, SIGNAL(error(QString)), SLOT(onError(QString))); dft->start(); return; } QByteArray urlstr = _src.toString(_src.FullyEncoded).toLatin1(); QString lowercaseurl = urlstr.toLower(); bool compressed = lowercaseurl.endsWith(".zip") || lowercaseurl.endsWith(".xz") || lowercaseurl.endsWith(".bz2") || lowercaseurl.endsWith(".gz") || lowercaseurl.endsWith(".7z") || lowercaseurl.endsWith(".cache"); if (!_extrLen && _src.isLocalFile()) { if (!compressed) _extrLen = _downloadLen; else if (lowercaseurl.endsWith(".zip")) _parseCompressedFile(); } if (_devLen && _extrLen > _devLen) { emit error(tr("Storage capacity is not large enough.\nNeeds to be at least %1 GB").arg(QString::number(_extrLen/1000000000.0, 'f', 1))); return; } if (_extrLen && !_multipleFilesInZip && _extrLen % 512 != 0) { emit error(tr("Input file is not a valid disk image.\nFile size %1 bytes is not a multiple of 512 bytes.").arg(_extrLen)); return; } if (!_expectedHash.isEmpty() && _cachedFileHash == _expectedHash) { // Use cached file urlstr = QUrl::fromLocalFile(_cacheFileName).toString(_src.FullyEncoded).toLatin1(); } if (compressed) { _thread = new DownloadExtractThread(urlstr, _dst.toLatin1(), _expectedHash, this); } else { _thread = new DownloadThread(urlstr, _dst.toLatin1(), _expectedHash, this); } _powersave.applyBlock(tr("Downloading and writing image")); connect(_thread, SIGNAL(success()), SLOT(onSuccess())); connect(_thread, SIGNAL(error(QString)), SLOT(onError(QString))); _thread->setVerifyEnabled(_verifyEnabled); if (!_expectedHash.isEmpty() && _cachedFileHash != _expectedHash && _cachingEnabled) { if (!_cachedFileHash.isEmpty()) { if (_settings.isWritable() && QFile::remove(_cacheFileName)) { _settings.remove("caching/lastDownloadSHA256"); _settings.sync(); _cachedFileHash.clear(); } else { qDebug() << "Error removing old cache file. Disabling caching"; _cachingEnabled = false; } } if (_cachingEnabled) { QStorageInfo si(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)); qint64 avail = si.bytesAvailable(); qDebug() << "Available disk space for caching:" << avail/1024/1024/1024 << "GB"; if (avail-_downloadLen < IMAGEWRITER_MINIMAL_SPACE_FOR_CACHING) { qDebug() << "Low disk space. Not caching files to disk."; } else { _thread->setCacheFile(_cacheFileName, _downloadLen); connect(_thread, SIGNAL(cacheFileUpdated(QByteArray)), SLOT(onCacheFileUpdated(QByteArray))); } } } if (_multipleFilesInZip) { static_cast(_thread)->enableMultipleFileExtraction(); DriveFormatThread *dft = new DriveFormatThread(_dst.toLatin1(), this); connect(dft, SIGNAL(success()), _thread, SLOT(start())); connect(dft, SIGNAL(error(QString)), SLOT(onError(QString))); dft->start(); } else { _thread->start(); } _polltimer.start(PROGRESS_UPDATE_INTERVAL); } void ImageWriter::onCacheFileUpdated(QByteArray sha256) { _settings.setValue("caching/lastDownloadSHA256", sha256); _settings.sync(); _cachedFileHash = sha256; qDebug() << "Done writing cache file"; } /* Cancel write */ void ImageWriter::cancelWrite() { if (_thread) { connect(_thread, SIGNAL(finished()), SLOT(onCancelled())); _thread->cancelDownload(); } if (!_thread || !_thread->isRunning()) { emit cancelled(); } } void ImageWriter::onCancelled() { sender()->deleteLater(); if (sender() == _thread) { _thread = nullptr; } emit cancelled(); } /* Return true if url is in our local disk cache */ bool ImageWriter::isCached(const QUrl &, const QByteArray &sha256) { return !sha256.isEmpty() && _cachedFileHash == sha256; } /* Utility function to return filename part from URL */ QString ImageWriter::fileNameFromUrl(const QUrl &url) { //return QFileInfo(url.toLocalFile()).fileName(); return url.fileName(); } QString ImageWriter::srcFileName() { return _src.isEmpty() ? "" : _src.fileName(); } /* Function to return OS list URL */ QUrl ImageWriter::constantOsListUrl() const { return _repo; } /* Function to return version */ QString ImageWriter::constantVersion() const { return IMAGER_VERSION_STR; } void ImageWriter::setCustomOsListUrl(const QUrl &url) { _repo = url; } /* Refresh the list of available drives */ void ImageWriter::refreshDriveList() { _drivelist.refreshDriveList(); } DriveListModel *ImageWriter::getDriveList() { return &_drivelist; } void ImageWriter::pollProgress() { if (!_thread) return; quint64 newDlNow, dlTotal; if (_extrLen) { newDlNow = _thread->bytesWritten(); dlTotal = _extrLen; } else { newDlNow = _thread->dlNow(); dlTotal = _thread->dlTotal(); } if (newDlNow != _dlnow) { _dlnow = newDlNow; emit downloadProgress(newDlNow, dlTotal); } quint64 newVerifyNow = _thread->verifyNow(); if (newVerifyNow != _verifynow) { _verifynow = newVerifyNow; quint64 verifyTotal = _thread->verifyTotal(); emit verifyProgress(newVerifyNow, verifyTotal); } } void ImageWriter::setVerifyEnabled(bool verify) { _verifyEnabled = verify; if (_thread) _thread->setVerifyEnabled(verify); } /* Relay events from download thread to QML */ void ImageWriter::onSuccess() { _polltimer.stop(); pollProgress(); _powersave.removeBlock(); emit success(); } void ImageWriter::onError(QString msg) { _polltimer.stop(); pollProgress(); _powersave.removeBlock(); emit error(msg); } void ImageWriter::onFinalizing() { _polltimer.stop(); emit finalizing(); } void ImageWriter::openFileDialog() { QFileDialog *fd = new QFileDialog(nullptr, tr("Select image"), QStandardPaths::writableLocation(QStandardPaths::DownloadLocation), "Image files (*.img *.zip *.gz *.xz);;All files (*.*)"); connect(fd, SIGNAL(fileSelected(QString)), SLOT(onFileSelected(QString))); if (_engine) { fd->createWinId(); QWindow *handle = fd->windowHandle(); QWindow *qmlwindow = qobject_cast(_engine->rootObjects().value(0)); if (qmlwindow) { handle->setTransientParent(qmlwindow); } } fd->show(); } void ImageWriter::onFileSelected(QString filename) { QFileInfo fi(filename); if (fi.isFile()) { emit fileSelected(QUrl::fromLocalFile(filename)); } else { qDebug() << "Item selected is not a regular file"; } sender()->deleteLater(); } void ImageWriter::_parseCompressedFile() { struct archive *a = archive_read_new(); struct archive_entry *entry; QByteArray fn = _src.toLocalFile().toLatin1(); int numFiles = 0; _extrLen = 0; archive_read_support_filter_all(a); archive_read_support_format_all(a); if (archive_read_open_filename(a, fn.data(), 10240) == ARCHIVE_OK) { while ( (archive_read_next_header(a, &entry)) == ARCHIVE_OK) { if (archive_entry_size(entry) > 0) { _extrLen += archive_entry_size(entry); numFiles++; } } } if (numFiles > 1) _multipleFilesInZip = true; qDebug() << "Parsed .zip file containing" << numFiles << "files, uncompressed size:" << _extrLen; } void MountUtilsLog(std::string msg) { qDebug() << "mountutils:" << msg.c_str(); }