diff --git a/domain-server/src/BackupHandler.h b/domain-server/src/BackupHandler.h index b790591bea..ad1fc6b793 100644 --- a/domain-server/src/BackupHandler.h +++ b/domain-server/src/BackupHandler.h @@ -95,7 +95,7 @@ public: zipFile.write(entitiesFile.readAll()); zipFile.close(); if (zipFile.getZipError() != UNZ_OK) { - qDebug() << "testCreate(): outFile.close(): " << zipFile.getZipError(); + qDebug() << "Failed to zip models.json.gz: " << zipFile.getZipError(); } } } @@ -107,7 +107,10 @@ public: return; } QuaZipFile zipFile { &zip }; - zipFile.open(QIODevice::ReadOnly); + if (!zipFile.open(QIODevice::ReadOnly)) { + qWarning() << "Failed to open models.json.gz in backup"; + return; + } auto data = zipFile.readAll(); QFile entitiesFile { _entitiesFilePath }; @@ -117,6 +120,10 @@ public: } zipFile.close(); + + if (zipFile.getZipError() != UNZ_OK) { + qDebug() << "Failed to zip models.json.gz: " << zipFile.getZipError(); + } } // Delete a skeleton backup diff --git a/domain-server/src/BackupSupervisor.h b/domain-server/src/BackupSupervisor.h index 1023622971..9fedcca19b 100644 --- a/domain-server/src/BackupSupervisor.h +++ b/domain-server/src/BackupSupervisor.h @@ -91,51 +91,4 @@ private: int _mappingRequestsInFlight { 0 }; }; - -#include -class AssetsBackupHandler { -public: - AssetsBackupHandler(BackupSupervisor* backupSupervisor) : _backupSupervisor(backupSupervisor) {} - - void loadBackup(QuaZip& zip) {} - - void createBackup(QuaZip& zip) { - quint64 lastRefreshTimestamp = _backupSupervisor->getLastRefreshTimestamp(); - AssetUtils::Mappings mappings = _backupSupervisor->getCurrentMappings(); - - if (lastRefreshTimestamp == 0) { - qWarning() << "Current mappings not yet loaded, "; - return; - } - - static constexpr quint64 MAX_REFRESH_TIME = 15 * 60 * 1000 * 1000; - if (usecTimestampNow() - lastRefreshTimestamp > MAX_REFRESH_TIME) { - qWarning() << "Backing up asset mappings that appear old."; - } - - QJsonObject jsonObject; - for (const auto& mapping : mappings) { - jsonObject.insert(mapping.first, mapping.second); - } - QJsonDocument document(jsonObject); - - QuaZipFile zipFile { &zip }; - if (!zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo("mappings.json"))) { - qDebug() << "testCreate(): outFile.open()"; - } - zipFile.write(document.toJson()); - zipFile.close(); - if (zipFile.getZipError() != UNZ_OK) { - qDebug() << "testCreate(): outFile.close(): " << zipFile.getZipError(); - } - } - - void recoverBackup(QuaZip& zip) {} - void deleteBackup(QuaZip& zip) {} - void consolidateBackup(QuaZip& zip) {} - -private: - BackupSupervisor* _backupSupervisor; -}; - #endif /* hifi_BackupSupervisor_h */ diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index b0a80531f8..29f6b7948f 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include "DomainServer.h" #include "DomainContentBackupManager.h" @@ -36,7 +37,8 @@ const int DomainContentBackupManager::DEFAULT_PERSIST_INTERVAL = 1000 * 30; // // Backup format looks like: daily_backup-TIMESTAMP.zip const static QString DATETIME_FORMAT { "yyyy-MM-dd_HH-mm-ss" }; const static QString DATETIME_FORMAT_RE("\\d{4}-\\d{2}-\\d{2}_\\d{2}-\\d{2}-\\d{2}"); - +static const QString AUTOMATIC_BACKUP_PREFIX{ "autobackup-" }; +static const QString MANUAL_BACKUP_PREFIX{ "backup-" }; void DomainContentBackupManager::addBackupHandler(BackupHandler handler) { _backupHandlers.push_back(std::move(handler)); } @@ -83,7 +85,7 @@ void DomainContentBackupManager::parseSettings(const QJsonObject& settings) { auto name = obj["Name"].toString(); auto format = obj["format"].toString(); - format = name.replace(" ", "_").toLower() + "-"; + format = name.replace(" ", "_").toLower(); qCDebug(domain_server) << " Name:" << name; qCDebug(domain_server) << " format:" << format; @@ -129,6 +131,14 @@ void DomainContentBackupManager::setup() { } bool DomainContentBackupManager::process() { + if (!_initialLoadComplete) { + QDir backupDir { _backupDirectory }; + if (!backupDir.exists()) { + backupDir.mkpath("."); + } + _initialLoadComplete = true; + } + if (isStillRunning()) { constexpr int64_t MSECS_TO_USECS = 1000; constexpr int64_t USECS_TO_SLEEP = 10 * MSECS_TO_USECS; // every 10ms @@ -140,7 +150,7 @@ bool DomainContentBackupManager::process() { if (sinceLastSave > intervalToCheck) { _lastCheck = now; - persist(); + backup(); } } @@ -149,32 +159,18 @@ bool DomainContentBackupManager::process() { void DomainContentBackupManager::aboutToFinish() { qCDebug(domain_server) << "Persist thread about to finish..."; - persist(); -} - -void DomainContentBackupManager::persist() { - QDir backupDir { _backupDirectory }; - backupDir.mkpath("."); - - // create our "lock" file to indicate we're saving. - QString lockFileName = _backupDirectory + "/running.lock"; - - std::ofstream lockFile(qPrintable(lockFileName), std::ios::out | std::ios::binary); - if (lockFile.is_open()) { - backup(); - - lockFile.close(); - remove(qPrintable(lockFileName)); - } + backup(); + qCDebug(domain_server) << "Persist thread done with about to finish..."; + _stopThread = true; } bool DomainContentBackupManager::getMostRecentBackup(const QString& format, QString& mostRecentBackupFileName, QDateTime& mostRecentBackupTime) { - QRegExp formatRE { QRegExp::escape(format) + "(" + DATETIME_FORMAT_RE + ")" + "\\.zip" }; + QRegExp formatRE { AUTOMATIC_BACKUP_PREFIX + QRegExp::escape(format) + "\\-(" + DATETIME_FORMAT_RE + ")" + "\\.zip" }; QStringList filters; - filters << format + "*.zip"; + filters << AUTOMATIC_BACKUP_PREFIX + format + "*.zip"; bool bestBackupFound = false; QString bestBackupFile; @@ -216,7 +212,32 @@ bool DomainContentBackupManager::getMostRecentBackup(const QString& format, return bestBackupFound; } +bool DomainContentBackupManager::deleteBackup(const QString& backupName) { + if (QThread::currentThread() != thread()) { + bool result{ false }; + BLOCKING_INVOKE_METHOD(this, "deleteBackup", + Q_RETURN_ARG(bool, result), + Q_ARG(const QString&, backupName)); + return result; + } + + QDir backupDir { _backupDirectory }; + QFile backupFile { backupDir.filePath(backupName) }; + if (backupFile.remove()) { + return true; + } + return false; +} + bool DomainContentBackupManager::recoverFromBackup(const QString& backupName) { + if (QThread::currentThread() != thread()) { + bool result{ false }; + BLOCKING_INVOKE_METHOD(this, "recoverFromBackup", + Q_RETURN_ARG(bool, result), + Q_ARG(const QString&, backupName)); + return result; + } + qDebug() << "Recoving from" << backupName; QDir backupDir { _backupDirectory }; @@ -226,7 +247,6 @@ bool DomainContentBackupManager::recoverFromBackup(const QString& backupName) { if (!zip.open(QuaZip::Mode::mdUnzip)) { qWarning() << "Failed to unzip file: " << backupName; backupFile.close(); - return false; } for (auto& handler : _backupHandlers) { @@ -234,11 +254,43 @@ bool DomainContentBackupManager::recoverFromBackup(const QString& backupName) { } backupFile.close(); + qDebug() << "Successfully recovered from " << backupName; + return true; + } else { + qWarning() << "Invalid id: " << backupName; + return false; + } +} + +std::vector DomainContentBackupManager::getAllBackups() { + std::vector backups; + + QDir backupDir { _backupDirectory }; + auto matchingFiles = + backupDir.entryInfoList({ AUTOMATIC_BACKUP_PREFIX + "*.zip", MANUAL_BACKUP_PREFIX + "*.zip" }, + QDir::Files | QDir::NoSymLinks, QDir::Name); + QString prefixFormat = "(" + QRegExp::escape(AUTOMATIC_BACKUP_PREFIX) + "|" + QRegExp::escape(MANUAL_BACKUP_PREFIX) + ")"; + QString nameFormat = "(.+)"; + QString dateTimeFormat = "(" + DATETIME_FORMAT_RE + ")"; + QRegExp backupNameFormat { prefixFormat + nameFormat + "-" + dateTimeFormat + "\\.zip" }; + + for (const auto& fileInfo : matchingFiles) { + auto fileName = fileInfo.fileName(); + if (backupNameFormat.exactMatch(fileName)) { + auto type = backupNameFormat.cap(1); + auto name = backupNameFormat.cap(2); + auto dateTime = backupNameFormat.cap(3); + auto createdAt = QDateTime::fromString(dateTime, DATETIME_FORMAT); + if (!createdAt.isValid()) { + continue; + } + + BackupItemInfo backup { fileInfo.fileName(), name, fileInfo.absoluteFilePath(), createdAt, type == MANUAL_BACKUP_PREFIX }; + backups.push_back(backup); + } } - qDebug() << "Successfully recovered from " << backupName; - - return true; + return backups; } void DomainContentBackupManager::removeOldBackupVersions(const BackupRule& rule) { @@ -247,9 +299,10 @@ void DomainContentBackupManager::removeOldBackupVersions(const BackupRule& rule) qCDebug(domain_server) << "Rolling old backup versions for rule" << rule.name; auto matchingFiles = - backupDir.entryInfoList({ rule.extensionFormat + "*.zip" }, QDir::Files | QDir::NoSymLinks, QDir::Name); + backupDir.entryInfoList({ AUTOMATIC_BACKUP_PREFIX + rule.extensionFormat + "*.zip" }, QDir::Files | QDir::NoSymLinks, QDir::Name); int backupsToDelete = matchingFiles.length() - rule.maxBackupVersions; + qCDebug(domain_server) << "Found" << matchingFiles.length() << "backups, deleting " << backupsToDelete << "backup(s)"; for (int i = 0; i < backupsToDelete; ++i) { auto fileInfo = matchingFiles[i].absoluteFilePath(); QFile backupFile(fileInfo); @@ -313,6 +366,7 @@ void DomainContentBackupManager::backup() { qCDebug(domain_server) << "Time since last backup [" << secondsSinceLastBackup << "] for rule [" << rule.name << "] exceeds backup interval [" << rule.intervalSeconds << "] doing backup now..."; +<<<<<<< HEAD auto timestamp = QDateTime::currentDateTime().toString(DATETIME_FORMAT); auto fileName = "backup-" + rule.extensionFormat + timestamp + ".zip"; QuaZip zip(_backupDirectory + "/" + fileName); @@ -323,11 +377,17 @@ void DomainContentBackupManager::backup() { for (auto& handler : _backupHandlers) { handler.createBackup(zip); +======= + bool success; + QString path; + std::tie(success, path) = createBackup(AUTOMATIC_BACKUP_PREFIX, rule.extensionFormat); + if (!success) { + qCWarning(domain_server) << "Failed to create backup for" << rule.name << "at" << path; + continue; +>>>>>>> dd86471a42... Add backup DS APIs } - zip.close(); - - qDebug() << "Created backup: " << fileName; + qDebug() << "Created backup: " << path; rule.lastBackupSeconds = nowSeconds; @@ -365,3 +425,27 @@ void DomainContentBackupManager::consolidate(QString fileName) { zip.close(); } } + +void DomainContentBackupManager::createManualBackup(const QString& name) { + createBackup(MANUAL_BACKUP_PREFIX, name); +} + +std::pair DomainContentBackupManager::createBackup(const QString& prefix, const QString& name) { + auto timestamp = QDateTime::currentDateTime().toString(DATETIME_FORMAT); + auto fileName = prefix + name + "-" + timestamp + ".zip"; + auto path = _backupDirectory + "/" + fileName; + QuaZip zip(path); + if (!zip.open(QuaZip::mdAdd)) { + qCWarning(domain_server) << "Failed to open zip file at " << path; + qCWarning(domain_server) << " ERROR:" << zip.getZipError(); + return { false, path }; + } + + for (auto& handler : _backupHandlers) { + handler.createBackup(zip); + } + + zip.close(); + + return { true, path }; +} diff --git a/domain-server/src/DomainContentBackupManager.h b/domain-server/src/DomainContentBackupManager.h index d0dd9cf2c6..461d4dd794 100644 --- a/domain-server/src/DomainContentBackupManager.h +++ b/domain-server/src/DomainContentBackupManager.h @@ -21,6 +21,14 @@ #include "BackupHandler.h" +struct BackupItemInfo { + QString id; + QString name; + QString absolutePath; + QDateTime createdAt; + bool isManualBackup; +}; + class DomainContentBackupManager : public GenericThread { Q_OBJECT public: @@ -41,12 +49,18 @@ public: bool debugTimestampNow = false); void addBackupHandler(BackupHandler handler); + bool isInitialLoadComplete() const { return _initialLoadComplete; } + std::vector getAllBackups(); void aboutToFinish(); /// call this to inform the persist thread that the owner is about to finish to support final persist void replaceData(QByteArray data); + void createManualBackup(const QString& name); + +public slots: bool recoverFromBackup(const QString& backupName); + bool deleteBackup(const QString& backupName); signals: void loadCompleted(); @@ -56,7 +70,6 @@ protected: virtual void setup() override; virtual bool process() override; - void persist(); void load(); void backup(); void consolidate(QString fileName); @@ -65,10 +78,13 @@ protected: int64_t getMostRecentBackupTimeInSecs(const QString& format); void parseSettings(const QJsonObject& settings); + std::pair createBackup(const QString& prefix, const QString& name); + private: QString _backupDirectory; std::vector _backupHandlers; int _persistInterval { 0 }; + bool _initialLoadComplete { false }; int64_t _lastCheck { 0 }; std::vector _backupRules; diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index fe6a303e08..1949c40566 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -300,7 +300,10 @@ DomainServer::DomainServer(int argc, char* argv[]) : _contentManager->addBackupHandler(new BackupSupervisor(getContentBackupDir())); _contentManager->initialize(true); - _contentManager->recoverFromBackup("backup-daily_rolling-2018-02-06_15-13-50.zip"); + qDebug() << "Existing backups:"; + for (auto& backup : _contentManager->getAllBackups()) { + qDebug() << " Backup: " << backup.name << backup.createdAt; + } } void DomainServer::parseCommandLine() { @@ -1736,6 +1739,12 @@ void DomainServer::processOctreeDataPersistMessage(QSharedPointerreadAll(); auto filePath = getEntitiesFilePath(); + QDir dir(getEntitiesDirPath()); + if (!dir.exists()) { + qCDebug(domain_server) << "Creating entities content directory:" << dir.absolutePath(); + dir.mkpath("."); + } + QFile f(filePath); if (f.open(QIODevice::WriteOnly)) { f.write(data); @@ -1746,12 +1755,12 @@ void DomainServer::processOctreeDataPersistMessage(QSharedPointerrespond(HTTPConnection::StatusCode200, nodesDocument.toJson(), qPrintable(JSON_MIME_TYPE)); + return true; + } else if (url.path().startsWith(URI_API_BACKUPS_RECOVER)) { + auto id = url.path().mid(QString(URI_API_BACKUPS_RECOVER).length()); + _contentManager->recoverFromBackup(id); + QJsonObject rootJSON; + rootJSON["success"] = true; + QJsonDocument docJSON(rootJSON); + connection->respond(HTTPConnection::StatusCode200, docJSON.toJson(), JSON_MIME_TYPE.toUtf8()); + return true; + } else if (url.path() == URI_API_BACKUPS) { + QJsonObject rootJSON; + QJsonArray backupsJSON; + + auto backups = _contentManager->getAllBackups(); + + for (const auto& backup : backups) { + QJsonObject obj; + obj["id"] = backup.id; + obj["name"] = backup.name; + obj["createdAtMillis"] = backup.createdAt.toMSecsSinceEpoch(); + obj["isManualBackup"] = backup.isManualBackup; + backupsJSON.push_back(obj); + } + + rootJSON["backups"] = backupsJSON; + QJsonDocument docJSON(rootJSON); + + connection->respond(HTTPConnection::StatusCode200, docJSON.toJson(), JSON_MIME_TYPE.toUtf8()); return true; } else if (url.path() == URI_RESTART) { connection->respond(HTTPConnection::StatusCode200); @@ -2213,6 +2254,20 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url return true; + } else if (url.path() == URI_API_BACKUPS) { + qDebug() << "GOt request to create a backup:"; + auto params = connection->parseUrlEncodedForm(); + auto it = params.find("name"); + if (it == params.end()) { + connection->respond(HTTPConnection::StatusCode400, "Bad request, missing `name`"); + return true; + } + + _contentManager->createManualBackup(it.value()); + + connection->respond(HTTPConnection::StatusCode200); + return true; + } else if (url.path() == "/domain_settings") { auto accessTokenVariant = valueForKeyPath(_settingsManager.getSettingsMap(), ACCESS_TOKEN_KEY_PATH); if (!accessTokenVariant) { @@ -2311,7 +2366,16 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url QRegExp allNodesDeleteRegex(ALL_NODE_DELETE_REGEX_STRING); QRegExp nodeDeleteRegex(NODE_DELETE_REGEX_STRING); - if (nodeDeleteRegex.indexIn(url.path()) != -1) { + if (url.path().startsWith(URI_API_BACKUPS_ID)) { + auto id = url.path().mid(QString(URI_API_BACKUPS_ID).length()); + auto success = _contentManager->deleteBackup(id); + QJsonObject rootJSON; + rootJSON["success"] = success; + QJsonDocument docJSON(rootJSON); + connection->respond(HTTPConnection::StatusCode200, docJSON.toJson(), JSON_MIME_TYPE.toUtf8()); + return true; + + } else if (nodeDeleteRegex.indexIn(url.path()) != -1) { // this is a request to DELETE one node by UUID // pull the captured string, if it exists diff --git a/libraries/embedded-webserver/src/HTTPConnection.cpp b/libraries/embedded-webserver/src/HTTPConnection.cpp index a61bc95f8b..6496cc3f68 100644 --- a/libraries/embedded-webserver/src/HTTPConnection.cpp +++ b/libraries/embedded-webserver/src/HTTPConnection.cpp @@ -133,12 +133,33 @@ QList HTTPConnection::parseFormData() const { } void HTTPConnection::respond(const char* code, const QByteArray& content, const char* contentType, const Headers& headers) { + QByteArray data(content); + auto device { std::unique_ptr(new QBuffer()) }; + device->setBuffer(new QByteArray(content)); + if (device->open(QIODevice::ReadOnly)) { + respond(code, std::move(device), contentType, headers); + } else { + qCritical() << "Error opening QBuffer to respond to " << _requestUrl.path(); + } +} + +void HTTPConnection::respond(const char* code, std::unique_ptr device, const char* contentType, const Headers& headers) { + _responseDevice = std::move(device); + _socket->write("HTTP/1.1 "); + + if (_responseDevice->isSequential()) { + qWarning() << "Error responding to HTTPConnection: sequential IO devices not supported"; + _socket->write(StatusCode500); + _socket->write("\r\n"); + _socket->disconnect(SIGNAL(readyRead()), this); + _socket->disconnectFromHost(); + return; + } + _socket->write(code); _socket->write("\r\n"); - int csize = content.size(); - for (Headers::const_iterator it = headers.constBegin(), end = headers.constEnd(); it != end; it++) { _socket->write(it.key()); @@ -146,6 +167,8 @@ void HTTPConnection::respond(const char* code, const QByteArray& content, const _socket->write(it.value()); _socket->write("\r\n"); } + + int csize = _responseDevice->size(); if (csize > 0) { _socket->write("Content-Length: "); _socket->write(QByteArray::number(csize)); @@ -157,20 +180,35 @@ void HTTPConnection::respond(const char* code, const QByteArray& content, const } _socket->write("Connection: close\r\n\r\n"); - if (csize > 0) { - _socket->write(content); + if (_responseDevice->atEnd()) { + _socket->disconnectFromHost(); + } else { + constexpr size_t HTTP_RESPONSE_CHUNK_SIZE = 1024 * 10; + int totalToBeWritten = csize; + connect(_socket, &QTcpSocket::bytesWritten, this, [this, totalToBeWritten](size_t bytes) mutable { + if (!_responseDevice->atEnd()) { + totalToBeWritten -= _socket->write(_responseDevice->read(HTTP_RESPONSE_CHUNK_SIZE)); + if (_responseDevice->atEnd()) { + _socket->disconnectFromHost(); + disconnect(_socket, &QTcpSocket::bytesWritten, this, nullptr); + } + } + }); + } // make sure we receive no further read notifications - _socket->disconnect(SIGNAL(readyRead()), this); - - _socket->disconnectFromHost(); + disconnect(_socket, &QTcpSocket::readyRead, this, nullptr); } void HTTPConnection::readRequest() { if (!_socket->canReadLine()) { return; } + if (!_requestUrl.isEmpty()) { + qDebug() << "Request URL was already set"; + return; + } // parse out the method and resource QByteArray line = _socket->readLine().trimmed(); if (line.startsWith("HEAD")) { @@ -249,6 +287,7 @@ void HTTPConnection::readContent() { if (_socket->bytesAvailable() < size) { return; } + qDebug() << "Reading content"; _socket->read(_requestContent.data(), size); _socket->disconnect(this, SLOT(readContent())); diff --git a/libraries/embedded-webserver/src/HTTPConnection.h b/libraries/embedded-webserver/src/HTTPConnection.h index 966fc26949..9c435b14a0 100644 --- a/libraries/embedded-webserver/src/HTTPConnection.h +++ b/libraries/embedded-webserver/src/HTTPConnection.h @@ -87,6 +87,9 @@ public: void respond (const char* code, const QByteArray& content = QByteArray(), const char* contentType = DefaultContentType, const Headers& headers = Headers()); + void respond (const char* code, std::unique_ptr device, + const char* contentType = DefaultContentType, + const Headers& headers = Headers()); protected slots: @@ -127,6 +130,9 @@ protected: /// The content of the request. QByteArray _requestContent; + + /// Response content + std::unique_ptr _responseDevice; }; #endif // hifi_HTTPConnection_h diff --git a/libraries/embedded-webserver/src/HTTPManager.cpp b/libraries/embedded-webserver/src/HTTPManager.cpp index fd127a2e92..bd1b545412 100644 --- a/libraries/embedded-webserver/src/HTTPManager.cpp +++ b/libraries/embedded-webserver/src/HTTPManager.cpp @@ -98,13 +98,14 @@ bool HTTPManager::handleHTTPRequest(HTTPConnection* connection, const QUrl& url, // file exists, serve it static QMimeDatabase mimeDatabase; - QFile localFile(filePath); - localFile.open(QIODevice::ReadOnly); - QByteArray localFileData = localFile.readAll(); + auto localFile = std::unique_ptr(new QFile(filePath)); + localFile->open(QIODevice::ReadOnly); + QByteArray localFileData; QFileInfo localFileInfo(filePath); if (localFileInfo.completeSuffix() == "shtml") { + localFileData = localFile->readAll(); // this is a file that may have some SSI statements // the only thing we support is the include directive, but check the contents for that @@ -153,8 +154,12 @@ bool HTTPManager::handleHTTPRequest(HTTPConnection* connection, const QUrl& url, ? QString { "text/html" } : mimeDatabase.mimeTypeForFile(filePath).name(); - connection->respond(HTTPConnection::StatusCode200, localFileData, qPrintable(mimeType)); - + if (localFileData.isNull()) { + connection->respond(HTTPConnection::StatusCode200, std::move(localFile), qPrintable(mimeType)); + } else { + connection->respond(HTTPConnection::StatusCode200, localFileData, qPrintable(mimeType)); + } + return true; } }