From 298c5efe69a1d347b84481f86d83dbf7509f8682 Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Thu, 8 Nov 2018 12:13:56 -0800 Subject: [PATCH 01/11] Simple demo of uploading file in chunks --- .../resources/web/content/js/content.js | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/domain-server/resources/web/content/js/content.js b/domain-server/resources/web/content/js/content.js index 346e846748..61fee425ff 100644 --- a/domain-server/resources/web/content/js/content.js +++ b/domain-server/resources/web/content/js/content.js @@ -14,6 +14,81 @@ $(document).ready(function(){ return html; } + function uploadInChunks(file) { + var fileSize = file.size; + var filename = file.name; + var CHUNK_SIZE = 16384; + + for(p = 0; p <= fileSize; p += CHUNK_SIZE) { + var chunk = file.slice(p, p + CHUNK_SIZE, file.type); + var chunkFormData = new FormData(); + chunkFormData.append('restore-file-chunk', chunk, filename); + var ajaxParams = { + url: '/content/upload', + type: 'POST', + async: false, + timeout: 60, + processData: false, + contentType: false, + data: chunkFormData + }; + var ajaxObject = $.ajax(ajaxParams); + + } + + + } + + function uploadNextChunk(file, offset) + { + var fileSize = file.size; + var filename = file.name; + + var CHUNK_SIZE = 16384; + if (offset == undefined) { + offset = 0; + } + var isFinal = fileSize - offset > CHUNK_SIZE ? false : true; + var nextChunkSize = Math.min(fileSize - offset, CHUNK_SIZE); + var chunk = file.slice(offset, offset + CHUNK_SIZE, file.type); + var chunkFormData = new FormData(); + + var formItemName = isFinal ? 'restore-file-chunk-final' : 'restore-file-chunk'; + chunkFormData.append(formItemName, chunk, filename); + var ajaxParams = { + url: '/content/upload', + type: 'POST', + timeout: 30000, + cache: false, + processData: false, + data: chunkFormData + }; + + var ajaxObject = $.ajax(ajaxParams); + ajaxObject.fail(function(jqXHR, textStatus, errorThrown) { + showErrorMessage( + "Error", + "There was a problem restoring domain content.\n" + + "Please ensure that the content archive or entity file is valid and try again." + ); + }); + + if (!isFinal) { + ajaxObject.done(function (data, textStatus, jqXHR) + {uploadNextChunk(file, offset + CHUNK_SIZE);}); + } else { + ajaxObject.done(function(data, textStatus, jqXHR) { + isRestoring = true; + + // immediately reload backup information since one should be restoring now + reloadBackupInformation(); + + swal.close(); + }); + } + + } + function setupBackupUpload() { // construct the HTML needed for the settings backup panel var html = "
"; @@ -56,6 +131,11 @@ $(document).ready(function(){ showSpinnerAlert("Uploading content to restore"); + + uploadNextChunk(files[0]); + return; + + // Previous one-upload method. $.ajax({ url: '/content/upload', type: 'POST', From bb60324335d4c733e3d8cd26bd0519b458b45731 Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Thu, 8 Nov 2018 14:01:27 -0800 Subject: [PATCH 02/11] Chunked content upload - working proof-of-concept --- .../resources/web/content/js/content.js | 5 +- domain-server/src/DomainServer.cpp | 76 ++++++++++++------- domain-server/src/DomainServer.h | 2 + 3 files changed, 54 insertions(+), 29 deletions(-) diff --git a/domain-server/resources/web/content/js/content.js b/domain-server/resources/web/content/js/content.js index 61fee425ff..d581ddaf85 100644 --- a/domain-server/resources/web/content/js/content.js +++ b/domain-server/resources/web/content/js/content.js @@ -44,7 +44,7 @@ $(document).ready(function(){ var fileSize = file.size; var filename = file.name; - var CHUNK_SIZE = 16384; + var CHUNK_SIZE = 65536; if (offset == undefined) { offset = 0; } @@ -58,9 +58,10 @@ $(document).ready(function(){ var ajaxParams = { url: '/content/upload', type: 'POST', - timeout: 30000, + timeout: 30000, // 30 s cache: false, processData: false, + contentType: false, data: chunkFormData }; diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index b540592d7e..fd378cb39b 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -2258,41 +2258,63 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url // check the file extension to see what kind of file this is // to make sure we handle this filetype for a content restore auto dispositionValue = QString(firstFormData.first.value("Content-Disposition")); - auto formDataFilenameRegex = QRegExp("filename=\"(.+)\""); - auto matchIndex = formDataFilenameRegex.indexIn(dispositionValue); + QRegExp formDataFieldsRegex(R":(name="(restore-file.*)".*filename="(.+)"):"); + auto matchIndex = formDataFieldsRegex.indexIn(dispositionValue); + QString formItemName = ""; QString uploadedFilename = ""; if (matchIndex != -1) { - uploadedFilename = formDataFilenameRegex.cap(1); + formItemName = formDataFieldsRegex.cap(1); + uploadedFilename = formDataFieldsRegex.cap(2); } - if (uploadedFilename.endsWith(".json", Qt::CaseInsensitive) - || uploadedFilename.endsWith(".json.gz", Qt::CaseInsensitive)) { - // invoke our method to hand the new octree file off to the octree server - QMetaObject::invokeMethod(this, "handleOctreeFileReplacement", - Qt::QueuedConnection, Q_ARG(QByteArray, firstFormData.second)); - - // respond with a 200 for success + _pendingUploadedContent += firstFormData.second; + if (formItemName == "restore-file-chunk") { + // Received another chunk connection->respond(HTTPConnection::StatusCode200); - } else if (uploadedFilename.endsWith(".zip", Qt::CaseInsensitive)) { - auto deferred = makePromise("recoverFromUploadedBackup"); + } else if (formItemName == "restore-file-chunk-final") { + if (uploadedFilename.endsWith(".json", Qt::CaseInsensitive) + || uploadedFilename.endsWith(".json.gz", Qt::CaseInsensitive)) { + // invoke our method to hand the new octree file off to the octree server + QMetaObject::invokeMethod(this, "handleOctreeFileReplacement", + Qt::QueuedConnection, Q_ARG(QByteArray, _pendingUploadedContent)); + _pendingUploadedContent.clear(); + // respond with a 200 for success + connection->respond(HTTPConnection::StatusCode200); + } else if (formItemName == "restore-file") { + if (uploadedFilename.endsWith(".json", Qt::CaseInsensitive) + || uploadedFilename.endsWith(".json.gz", Qt::CaseInsensitive)) { + // invoke our method to hand the new octree file off to the octree server + QMetaObject::invokeMethod(this, "handleOctreeFileReplacement", + Qt::QueuedConnection, Q_ARG(QByteArray, firstFormData.second)); - deferred->then([connectionPtr, JSON_MIME_TYPE](QString error, QVariantMap result) { - if (!connectionPtr) { - return; + _pendingUploadedContent.clear(); + // respond with a 200 for success + connection->respond(HTTPConnection::StatusCode200); + } else if (uploadedFilename.endsWith(".zip", Qt::CaseInsensitive)) { + auto deferred = makePromise("recoverFromUploadedBackup"); + + deferred->then([connectionPtr, JSON_MIME_TYPE](QString error, QVariantMap result) { + if (!connectionPtr) { + return; + } + + QJsonObject rootJSON; + auto success = result["success"].toBool(); + rootJSON["success"] = success; + QJsonDocument docJSON(rootJSON); + connectionPtr->respond(success ? HTTPConnection::StatusCode200 : HTTPConnection::StatusCode400, docJSON.toJson(), + JSON_MIME_TYPE.toUtf8()); + }); + + _contentManager->recoverFromUploadedBackup(deferred, firstFormData.second); + _pendingUploadedContent.clear(); + + return true; } - - QJsonObject rootJSON; - auto success = result["success"].toBool(); - rootJSON["success"] = success; - QJsonDocument docJSON(rootJSON); - connectionPtr->respond(success ? HTTPConnection::StatusCode200 : HTTPConnection::StatusCode400, docJSON.toJson(), - JSON_MIME_TYPE.toUtf8()); - }); - - _contentManager->recoverFromUploadedBackup(deferred, firstFormData.second); - - return true; + } else { + connection->respond(HTTPConnection::StatusCode400); + } } else { // we don't have handling for this filetype, send back a 400 for failure connection->respond(HTTPConnection::StatusCode400); diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index e2bddc1aa5..db50af546e 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -281,6 +281,8 @@ private: QHash> _pendingOAuthConnections; + QByteArray _pendingUploadedContent; + QThread _assetClientThread; }; From a21d10ad1b086ae932f1db2a3b05f85306b3daa7 Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Thu, 8 Nov 2018 16:34:02 -0800 Subject: [PATCH 03/11] Restore content archives (zip) correctly; other tweaks --- .../resources/web/content/js/content.js | 63 ++------------- domain-server/src/DomainServer.cpp | 78 +++++++++---------- domain-server/src/DomainServer.h | 2 + 3 files changed, 43 insertions(+), 100 deletions(-) diff --git a/domain-server/resources/web/content/js/content.js b/domain-server/resources/web/content/js/content.js index d581ddaf85..38f93a57be 100644 --- a/domain-server/resources/web/content/js/content.js +++ b/domain-server/resources/web/content/js/content.js @@ -14,41 +14,18 @@ $(document).ready(function(){ return html; } - function uploadInChunks(file) { - var fileSize = file.size; - var filename = file.name; - var CHUNK_SIZE = 16384; - - for(p = 0; p <= fileSize; p += CHUNK_SIZE) { - var chunk = file.slice(p, p + CHUNK_SIZE, file.type); - var chunkFormData = new FormData(); - chunkFormData.append('restore-file-chunk', chunk, filename); - var ajaxParams = { - url: '/content/upload', - type: 'POST', - async: false, - timeout: 60, - processData: false, - contentType: false, - data: chunkFormData - }; - var ajaxObject = $.ajax(ajaxParams); - - } - - - } - function uploadNextChunk(file, offset) { + if (offset == undefined) { + offset = 0; + } + var fileSize = file.size; var filename = file.name; var CHUNK_SIZE = 65536; - if (offset == undefined) { - offset = 0; - } - var isFinal = fileSize - offset > CHUNK_SIZE ? false : true; + + var isFinal = Boolean(fileSize - offset <= CHUNK_SIZE); var nextChunkSize = Math.min(fileSize - offset, CHUNK_SIZE); var chunk = file.slice(offset, offset + CHUNK_SIZE, file.type); var chunkFormData = new FormData(); @@ -127,38 +104,10 @@ $(document).ready(function(){ function() { var files = $('#' + RESTORE_SETTINGS_FILE_ID).prop('files'); - var fileFormData = new FormData(); - fileFormData.append('restore-file', files[0]); - showSpinnerAlert("Uploading content to restore"); uploadNextChunk(files[0]); - return; - - // Previous one-upload method. - $.ajax({ - url: '/content/upload', - type: 'POST', - timeout: 3600000, // Set timeout to 1h - cache: false, - processData: false, - contentType: false, - data: fileFormData - }).done(function(data, textStatus, jqXHR) { - isRestoring = true; - - // immediately reload backup information since one should be restoring now - reloadBackupInformation(); - - swal.close(); - }).fail(function(jqXHR, textStatus, errorThrown) { - showErrorMessage( - "Error", - "There was a problem restoring domain content.\n" - + "Please ensure that the content archive or entity file is valid and try again." - ); - }); } ); }); diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index fd378cb39b..117313462b 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -2273,53 +2273,12 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url // Received another chunk connection->respond(HTTPConnection::StatusCode200); } else if (formItemName == "restore-file-chunk-final") { - if (uploadedFilename.endsWith(".json", Qt::CaseInsensitive) - || uploadedFilename.endsWith(".json.gz", Qt::CaseInsensitive)) { - // invoke our method to hand the new octree file off to the octree server - QMetaObject::invokeMethod(this, "handleOctreeFileReplacement", - Qt::QueuedConnection, Q_ARG(QByteArray, _pendingUploadedContent)); - _pendingUploadedContent.clear(); - // respond with a 200 for success - connection->respond(HTTPConnection::StatusCode200); + readPendingContent(connection, uploadedFilename); } else if (formItemName == "restore-file") { - if (uploadedFilename.endsWith(".json", Qt::CaseInsensitive) - || uploadedFilename.endsWith(".json.gz", Qt::CaseInsensitive)) { - // invoke our method to hand the new octree file off to the octree server - QMetaObject::invokeMethod(this, "handleOctreeFileReplacement", - Qt::QueuedConnection, Q_ARG(QByteArray, firstFormData.second)); - - _pendingUploadedContent.clear(); - // respond with a 200 for success - connection->respond(HTTPConnection::StatusCode200); - } else if (uploadedFilename.endsWith(".zip", Qt::CaseInsensitive)) { - auto deferred = makePromise("recoverFromUploadedBackup"); - - deferred->then([connectionPtr, JSON_MIME_TYPE](QString error, QVariantMap result) { - if (!connectionPtr) { - return; - } - - QJsonObject rootJSON; - auto success = result["success"].toBool(); - rootJSON["success"] = success; - QJsonDocument docJSON(rootJSON); - connectionPtr->respond(success ? HTTPConnection::StatusCode200 : HTTPConnection::StatusCode400, docJSON.toJson(), - JSON_MIME_TYPE.toUtf8()); - }); - - _contentManager->recoverFromUploadedBackup(deferred, firstFormData.second); - _pendingUploadedContent.clear(); - - return true; - } - } else { - connection->respond(HTTPConnection::StatusCode400); - } + readPendingContent(connection, uploadedFilename); } else { - // we don't have handling for this filetype, send back a 400 for failure connection->respond(HTTPConnection::StatusCode400); } - } else { // respond with a 400 for failure connection->respond(HTTPConnection::StatusCode400); @@ -2568,6 +2527,39 @@ bool DomainServer::handleHTTPSRequest(HTTPSConnection* connection, const QUrl &u } } +void DomainServer::readPendingContent(HTTPConnection* connection, QString filename) { + if (filename.endsWith(".json", Qt::CaseInsensitive) + || filename.endsWith(".json.gz", Qt::CaseInsensitive)) { + // invoke our method to hand the new octree file off to the octree server + QMetaObject::invokeMethod(this, "handleOctreeFileReplacement", + Qt::QueuedConnection, Q_ARG(QByteArray, _pendingUploadedContent)); + + // respond with a 200 for success + connection->respond(HTTPConnection::StatusCode200); + _pendingUploadedContent.clear(); + } else if (filename.endsWith(".zip", Qt::CaseInsensitive)) { + auto deferred = makePromise("recoverFromUploadedBackup"); + + QPointer connectionPtr(connection); + const QString JSON_MIME_TYPE = "application/json"; + deferred->then([connectionPtr, JSON_MIME_TYPE, this](QString error, QVariantMap result) { + if (!connectionPtr) { + return; + } + + QJsonObject rootJSON; + auto success = result["success"].toBool(); + rootJSON["success"] = success; + QJsonDocument docJSON(rootJSON); + connectionPtr->respond(success ? HTTPConnection::StatusCode200 : HTTPConnection::StatusCode400, docJSON.toJson(), + JSON_MIME_TYPE.toUtf8()); + _pendingUploadedContent.clear(); + }); + + _contentManager->recoverFromUploadedBackup(deferred, _pendingUploadedContent); + } +} + HTTPSConnection* DomainServer::connectionFromReplyWithState(QNetworkReply* reply) { // grab the UUID state property from the reply QUuid stateUUID = reply->property(STATE_QUERY_KEY.toLocal8Bit()).toUuid(); diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index db50af546e..b2ef933bc3 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -209,6 +209,8 @@ private: HTTPSConnection* connectionFromReplyWithState(QNetworkReply* reply); + void readPendingContent(HTTPConnection* connection, QString filename); + bool forwardMetaverseAPIRequest(HTTPConnection* connection, const QString& metaversePath, const QString& requestSubobject, From dab025304d23c6af5e5bfaaa165841cb79148c52 Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Tue, 13 Nov 2018 12:22:30 -0800 Subject: [PATCH 04/11] Set chunk size to 1 MB --- domain-server/resources/web/content/js/content.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/domain-server/resources/web/content/js/content.js b/domain-server/resources/web/content/js/content.js index 38f93a57be..3f36a1b447 100644 --- a/domain-server/resources/web/content/js/content.js +++ b/domain-server/resources/web/content/js/content.js @@ -23,7 +23,7 @@ $(document).ready(function(){ var fileSize = file.size; var filename = file.name; - var CHUNK_SIZE = 65536; + var CHUNK_SIZE = 1048576; // 1 MiB var isFinal = Boolean(fileSize - offset <= CHUNK_SIZE); var nextChunkSize = Math.min(fileSize - offset, CHUNK_SIZE); From f5e14565b804f92c9a86a55edf8a23a015b92bfa Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Wed, 14 Nov 2018 10:17:48 -0800 Subject: [PATCH 05/11] Clean-up javascript formatting --- .../resources/web/content/js/content.js | 46 +++++++++---------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/domain-server/resources/web/content/js/content.js b/domain-server/resources/web/content/js/content.js index 3f36a1b447..93bde3122b 100644 --- a/domain-server/resources/web/content/js/content.js +++ b/domain-server/resources/web/content/js/content.js @@ -14,17 +14,16 @@ $(document).ready(function(){ return html; } - function uploadNextChunk(file, offset) - { + function uploadNextChunk(file, offset) { if (offset == undefined) { offset = 0; } var fileSize = file.size; var filename = file.name; - + var CHUNK_SIZE = 1048576; // 1 MiB - + var isFinal = Boolean(fileSize - offset <= CHUNK_SIZE); var nextChunkSize = Math.min(fileSize - offset, CHUNK_SIZE); var chunk = file.slice(offset, offset + CHUNK_SIZE, file.type); @@ -33,29 +32,29 @@ $(document).ready(function(){ var formItemName = isFinal ? 'restore-file-chunk-final' : 'restore-file-chunk'; chunkFormData.append(formItemName, chunk, filename); var ajaxParams = { - url: '/content/upload', - type: 'POST', - timeout: 30000, // 30 s - cache: false, - processData: false, - contentType: false, - data: chunkFormData - }; - + url: '/content/upload', + type: 'POST', + timeout: 30000, // 30 s + cache: false, + processData: false, + contentType: false, + data: chunkFormData + }; + var ajaxObject = $.ajax(ajaxParams); ajaxObject.fail(function(jqXHR, textStatus, errorThrown) { - showErrorMessage( - "Error", - "There was a problem restoring domain content.\n" - + "Please ensure that the content archive or entity file is valid and try again." - ); - }); + showErrorMessage( + "Error", + "There was a problem restoring domain content.\n" + + "Please ensure that the content archive or entity file is valid and try again." + ); + }); if (!isFinal) { - ajaxObject.done(function (data, textStatus, jqXHR) - {uploadNextChunk(file, offset + CHUNK_SIZE);}); + ajaxObject.done(function (data, textStatus, jqXHR) + { uploadNextChunk(file, offset + CHUNK_SIZE); }); } else { - ajaxObject.done(function(data, textStatus, jqXHR) { + ajaxObject.done(function(data, textStatus, jqXHR) { isRestoring = true; // immediately reload backup information since one should be restoring now @@ -65,7 +64,7 @@ $(document).ready(function(){ }); } - } + } function setupBackupUpload() { // construct the HTML needed for the settings backup panel @@ -106,7 +105,6 @@ $(document).ready(function(){ showSpinnerAlert("Uploading content to restore"); - uploadNextChunk(files[0]); } ); From cd00abd2163583b68296443e30f1c113218bff15 Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Thu, 15 Nov 2018 09:47:43 -0800 Subject: [PATCH 06/11] Add upload progress bar --- .../resources/web/content/js/content.js | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/domain-server/resources/web/content/js/content.js b/domain-server/resources/web/content/js/content.js index 93bde3122b..78e78266d3 100644 --- a/domain-server/resources/web/content/js/content.js +++ b/domain-server/resources/web/content/js/content.js @@ -10,10 +10,20 @@ $(document).ready(function(){ function progressBarHTML(extraClass, label) { var html = "
"; html += "
"; - html += label + "
"; + html += label + "
"; return html; } + function showUploadProgress(title) { + swal({ + title: title, + text: progressBarHTML('upload-content-progress', 'Upload'), + html: true, + showConfirmButton: false, + allowEscapeKey: false + }); + } + function uploadNextChunk(file, offset) { if (offset == undefined) { offset = 0; @@ -26,7 +36,7 @@ $(document).ready(function(){ var isFinal = Boolean(fileSize - offset <= CHUNK_SIZE); var nextChunkSize = Math.min(fileSize - offset, CHUNK_SIZE); - var chunk = file.slice(offset, offset + CHUNK_SIZE, file.type); + var chunk = file.slice(offset, offset + nextChunkSize, file.type); var chunkFormData = new FormData(); var formItemName = isFinal ? 'restore-file-chunk-final' : 'restore-file-chunk'; @@ -50,6 +60,8 @@ $(document).ready(function(){ ); }); + updateProgressBars($('.upload-content-progress'), Math.round((offset + nextChunkSize) * 100 / fileSize)); + if (!isFinal) { ajaxObject.done(function (data, textStatus, jqXHR) { uploadNextChunk(file, offset + CHUNK_SIZE); }); @@ -102,10 +114,10 @@ $(document).ready(function(){ "Restore content", function() { var files = $('#' + RESTORE_SETTINGS_FILE_ID).prop('files'); + var file = files[0]; - showSpinnerAlert("Uploading content to restore"); - - uploadNextChunk(files[0]); + showUploadProgress("Uploading " + file.name); + uploadNextChunk(file); } ); }); @@ -196,6 +208,11 @@ $(document).ready(function(){ checkBackupStatus(); }); + function updateProgressBars($progressBar, value) { + $progressBar.attr('aria-valuenow', value).attr('style', 'width: ' + value + '%'); + $progressBar.find('.ongoing-msg').html(" " + value + "% Complete"); + } + function reloadBackupInformation() { // make a GET request to get backup information to populate the table $.ajax({ @@ -232,11 +249,6 @@ $(document).ready(function(){ + "
  • Delete
  • "; } - function updateProgressBars($progressBar, value) { - $progressBar.attr('aria-valuenow', value).attr('style', 'width: ' + value + '%'); - $progressBar.find('.sr-only').html(value + "% Complete"); - } - // before we add any new rows and update existing ones // remove our flag for active rows $('.' + ACTIVE_BACKUP_ROW_CLASS).removeClass(ACTIVE_BACKUP_ROW_CLASS); From 22c3f5239adbc48fbd3ed05b4617a8f09cd1a0b1 Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Thu, 15 Nov 2018 16:59:52 -0800 Subject: [PATCH 07/11] Relay upoaded content.zip chunks to temp file Entities uploads still build in-memory. Move out chunk handling to new routine. --- .../resources/web/content/js/content.js | 4 +- .../src/DomainContentBackupManager.cpp | 21 +++++ .../src/DomainContentBackupManager.h | 1 + domain-server/src/DomainServer.cpp | 77 ++++++++++--------- domain-server/src/DomainServer.h | 3 +- 5 files changed, 67 insertions(+), 39 deletions(-) diff --git a/domain-server/resources/web/content/js/content.js b/domain-server/resources/web/content/js/content.js index 78e78266d3..365c5e8403 100644 --- a/domain-server/resources/web/content/js/content.js +++ b/domain-server/resources/web/content/js/content.js @@ -60,7 +60,7 @@ $(document).ready(function(){ ); }); - updateProgressBars($('.upload-content-progress'), Math.round((offset + nextChunkSize) * 100 / fileSize)); + updateProgressBars($('.upload-content-progress'), (offset + nextChunkSize) * 100 / fileSize); if (!isFinal) { ajaxObject.done(function (data, textStatus, jqXHR) @@ -210,7 +210,7 @@ $(document).ready(function(){ function updateProgressBars($progressBar, value) { $progressBar.attr('aria-valuenow', value).attr('style', 'width: ' + value + '%'); - $progressBar.find('.ongoing-msg').html(" " + value + "% Complete"); + $progressBar.find('.ongoing-msg').html(" " + Math.round(value) + "% Complete"); } function reloadBackupInformation() { diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index 518ed73f9e..3b8180e49e 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -348,6 +348,27 @@ void DomainContentBackupManager::recoverFromUploadedBackup(MiniPromise::Promise }); } +void DomainContentBackupManager::recoverFromUploadedFile(MiniPromise::Promise promise, QString uploadedFilename) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "recoverFromUploadedFile", Q_ARG(MiniPromise::Promise, promise), + Q_ARG(QString, uploadedFilename)); + return; + } + + qDebug() << "Recovering from uploaded file -" << uploadedFilename; + + QFile uploadedFile(uploadedFilename); + QuaZip uploadedZip { &uploadedFile }; + + QString backupName = MANUAL_BACKUP_PREFIX + "uploaded.zip"; + + bool success = recoverFromBackupZip(backupName, uploadedZip); + + promise->resolve({ + { "success", success } + }); +} + std::vector DomainContentBackupManager::getAllBackups() { QDir backupDir { _backupDirectory }; diff --git a/domain-server/src/DomainContentBackupManager.h b/domain-server/src/DomainContentBackupManager.h index 2b07afe0b3..4af3ae5bfd 100644 --- a/domain-server/src/DomainContentBackupManager.h +++ b/domain-server/src/DomainContentBackupManager.h @@ -86,6 +86,7 @@ public slots: void createManualBackup(MiniPromise::Promise promise, const QString& name); void recoverFromBackup(MiniPromise::Promise promise, const QString& backupName); void recoverFromUploadedBackup(MiniPromise::Promise promise, QByteArray uploadedBackup); + void recoverFromUploadedFile(MiniPromise::Promise promise, QString uploadedFilename); void deleteBackup(MiniPromise::Promise promise, const QString& backupName); signals: diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 117313462b..3e68cd0fc4 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -2268,17 +2268,8 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url uploadedFilename = formDataFieldsRegex.cap(2); } - _pendingUploadedContent += firstFormData.second; - if (formItemName == "restore-file-chunk") { - // Received another chunk - connection->respond(HTTPConnection::StatusCode200); - } else if (formItemName == "restore-file-chunk-final") { - readPendingContent(connection, uploadedFilename); - } else if (formItemName == "restore-file") { - readPendingContent(connection, uploadedFilename); - } else { - connection->respond(HTTPConnection::StatusCode400); - } + // Received a chunk + processPendingContent(connection, formItemName, uploadedFilename, firstFormData.second); } else { // respond with a 400 for failure connection->respond(HTTPConnection::StatusCode400); @@ -2527,37 +2518,51 @@ bool DomainServer::handleHTTPSRequest(HTTPSConnection* connection, const QUrl &u } } -void DomainServer::readPendingContent(HTTPConnection* connection, QString filename) { - if (filename.endsWith(".json", Qt::CaseInsensitive) - || filename.endsWith(".json.gz", Qt::CaseInsensitive)) { - // invoke our method to hand the new octree file off to the octree server - QMetaObject::invokeMethod(this, "handleOctreeFileReplacement", - Qt::QueuedConnection, Q_ARG(QByteArray, _pendingUploadedContent)); +bool DomainServer::processPendingContent(HTTPConnection* connection, QString itemName, QString filename, QByteArray dataChunk) { + if (filename.endsWith(".zip", Qt::CaseInsensitive)) { + static const QString TEMPORARY_CONTENT_FILEPATH { QDir::tempPath() + "/hifiUploadContent_XXXXXX.zip" }; - // respond with a 200 for success + if (!_pendingFileContent) { + _pendingFileContent = std::make_unique(TEMPORARY_CONTENT_FILEPATH); + } + if (!_pendingFileContent->open()) { + _pendingFileContent = nullptr; + connection->respond(HTTPConnection::StatusCode400); + return false; + } + _pendingFileContent->seek(_pendingFileContent->size()); + _pendingFileContent->write(dataChunk); + _pendingFileContent->close(); + + // Respond immediately - will timeout if we wait for restore. connection->respond(HTTPConnection::StatusCode200); - _pendingUploadedContent.clear(); - } else if (filename.endsWith(".zip", Qt::CaseInsensitive)) { - auto deferred = makePromise("recoverFromUploadedBackup"); + if (itemName == "restore-file-chunk-final" || itemName == "restore-file") { + auto deferred = makePromise("recoverFromUploadedBackup"); - QPointer connectionPtr(connection); - const QString JSON_MIME_TYPE = "application/json"; - deferred->then([connectionPtr, JSON_MIME_TYPE, this](QString error, QVariantMap result) { - if (!connectionPtr) { - return; - } + deferred->then([this](QString error, QVariantMap result) { + _pendingFileContent = nullptr; + }); - QJsonObject rootJSON; - auto success = result["success"].toBool(); - rootJSON["success"] = success; - QJsonDocument docJSON(rootJSON); - connectionPtr->respond(success ? HTTPConnection::StatusCode200 : HTTPConnection::StatusCode400, docJSON.toJson(), - JSON_MIME_TYPE.toUtf8()); + _contentManager->recoverFromUploadedFile(deferred, _pendingFileContent->fileName()); + } else { + } + } else if (filename.endsWith(".json", Qt::CaseInsensitive) + || filename.endsWith(".json.gz", Qt::CaseInsensitive)) { + _pendingUploadedContent += dataChunk; + connection->respond(HTTPConnection::StatusCode200); + + if (itemName == "restore-file-chunk-final" || itemName == "restore-file") { + // invoke our method to hand the new octree file off to the octree server + QMetaObject::invokeMethod(this, "handleOctreeFileReplacement", + Qt::QueuedConnection, Q_ARG(QByteArray, _pendingUploadedContent)); _pendingUploadedContent.clear(); - }); - - _contentManager->recoverFromUploadedBackup(deferred, _pendingUploadedContent); + } + } else { + connection->respond(HTTPConnection::StatusCode400); + return false; } + + return true; } HTTPSConnection* DomainServer::connectionFromReplyWithState(QNetworkReply* reply) { diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index b2ef933bc3..73e23bb3fe 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -209,7 +209,7 @@ private: HTTPSConnection* connectionFromReplyWithState(QNetworkReply* reply); - void readPendingContent(HTTPConnection* connection, QString filename); + bool processPendingContent(HTTPConnection* connection, QString itemName, QString filename, QByteArray dataChunk); bool forwardMetaverseAPIRequest(HTTPConnection* connection, const QString& metaversePath, @@ -284,6 +284,7 @@ private: QHash> _pendingOAuthConnections; QByteArray _pendingUploadedContent; + std::unique_ptr _pendingFileContent; QThread _assetClientThread; }; From 5f51ed0210b24393098f9cd46620db07bb1eda8c Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Fri, 16 Nov 2018 09:44:21 -0800 Subject: [PATCH 08/11] Keep pending content per remote address --- domain-server/src/DomainServer.cpp | 28 ++++++++++++++++------------ domain-server/src/DomainServer.h | 5 +++-- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 3e68cd0fc4..670e179f81 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -2521,33 +2521,37 @@ bool DomainServer::handleHTTPSRequest(HTTPSConnection* connection, const QUrl &u bool DomainServer::processPendingContent(HTTPConnection* connection, QString itemName, QString filename, QByteArray dataChunk) { if (filename.endsWith(".zip", Qt::CaseInsensitive)) { static const QString TEMPORARY_CONTENT_FILEPATH { QDir::tempPath() + "/hifiUploadContent_XXXXXX.zip" }; + const auto peerAddressHash = qHash(connection->socket()->peerAddress()); - if (!_pendingFileContent) { - _pendingFileContent = std::make_unique(TEMPORARY_CONTENT_FILEPATH); + if (_pendingContentFiles.find(peerAddressHash) == _pendingContentFiles.end()) { + _pendingContentFiles.emplace(peerAddressHash, TEMPORARY_CONTENT_FILEPATH); } - if (!_pendingFileContent->open()) { - _pendingFileContent = nullptr; + + QTemporaryFile& _pendingFileContent = _pendingContentFiles[peerAddressHash]; + if (!_pendingFileContent.open()) { + _pendingContentFiles.erase(peerAddressHash); connection->respond(HTTPConnection::StatusCode400); return false; } - _pendingFileContent->seek(_pendingFileContent->size()); - _pendingFileContent->write(dataChunk); - _pendingFileContent->close(); + _pendingFileContent.seek(_pendingFileContent.size()); + _pendingFileContent.write(dataChunk); + _pendingFileContent.close(); // Respond immediately - will timeout if we wait for restore. connection->respond(HTTPConnection::StatusCode200); if (itemName == "restore-file-chunk-final" || itemName == "restore-file") { auto deferred = makePromise("recoverFromUploadedBackup"); - deferred->then([this](QString error, QVariantMap result) { - _pendingFileContent = nullptr; + deferred->then([this, peerAddressHash](QString error, QVariantMap result) { + _pendingContentFiles.erase(peerAddressHash); }); - _contentManager->recoverFromUploadedFile(deferred, _pendingFileContent->fileName()); - } else { + _contentManager->recoverFromUploadedFile(deferred, _pendingFileContent.fileName()); } } else if (filename.endsWith(".json", Qt::CaseInsensitive) || filename.endsWith(".json.gz", Qt::CaseInsensitive)) { + auto peerAddressHash = qHash(connection->socket()->peerAddress()); + QByteArray& _pendingUploadedContent = _pendingUploadedContents[peerAddressHash]; _pendingUploadedContent += dataChunk; connection->respond(HTTPConnection::StatusCode200); @@ -2555,7 +2559,7 @@ bool DomainServer::processPendingContent(HTTPConnection* connection, QString ite // invoke our method to hand the new octree file off to the octree server QMetaObject::invokeMethod(this, "handleOctreeFileReplacement", Qt::QueuedConnection, Q_ARG(QByteArray, _pendingUploadedContent)); - _pendingUploadedContent.clear(); + _pendingUploadedContents.erase(peerAddressHash); } } else { connection->respond(HTTPConnection::StatusCode400); diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index 73e23bb3fe..2d5729be59 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -20,6 +20,7 @@ #include #include #include +#include #include #include @@ -283,8 +284,8 @@ private: QHash> _pendingOAuthConnections; - QByteArray _pendingUploadedContent; - std::unique_ptr _pendingFileContent; + std::unordered_map _pendingUploadedContents; + std::unordered_map _pendingContentFiles; QThread _assetClientThread; }; From 588303599185bb1adbd43da95b7a41d3db42fd15 Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Fri, 16 Nov 2018 14:50:03 -0800 Subject: [PATCH 09/11] Gcc doesn't like QTemporaryFile in a map so store unique ptrs --- domain-server/src/DomainServer.cpp | 5 +++-- domain-server/src/DomainServer.h | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 670e179f81..20ca5c94d4 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -2524,10 +2524,11 @@ bool DomainServer::processPendingContent(HTTPConnection* connection, QString ite const auto peerAddressHash = qHash(connection->socket()->peerAddress()); if (_pendingContentFiles.find(peerAddressHash) == _pendingContentFiles.end()) { - _pendingContentFiles.emplace(peerAddressHash, TEMPORARY_CONTENT_FILEPATH); + std::unique_ptr newTemp(new QTemporaryFile(TEMPORARY_CONTENT_FILEPATH)); + _pendingContentFiles[peerAddressHash] = std::move(newTemp); } - QTemporaryFile& _pendingFileContent = _pendingContentFiles[peerAddressHash]; + QTemporaryFile& _pendingFileContent = *_pendingContentFiles[peerAddressHash]; if (!_pendingFileContent.open()) { _pendingContentFiles.erase(peerAddressHash); connection->respond(HTTPConnection::StatusCode400); diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index 2d5729be59..5d6cd4e5f9 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -285,7 +285,7 @@ private: QHash> _pendingOAuthConnections; std::unordered_map _pendingUploadedContents; - std::unordered_map _pendingContentFiles; + std::unordered_map> _pendingContentFiles; QThread _assetClientThread; }; From 2d2cc0eaca55a14af050e622d63168872d6f6431 Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Fri, 16 Nov 2018 15:56:39 -0800 Subject: [PATCH 10/11] Use a random session id for a sequence of chunks --- .../resources/web/content/js/content.js | 13 +++++++---- domain-server/src/DomainServer.cpp | 22 ++++++++++--------- domain-server/src/DomainServer.h | 4 ++-- 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/domain-server/resources/web/content/js/content.js b/domain-server/resources/web/content/js/content.js index 365c5e8403..afee2cc8a9 100644 --- a/domain-server/resources/web/content/js/content.js +++ b/domain-server/resources/web/content/js/content.js @@ -10,7 +10,7 @@ $(document).ready(function(){ function progressBarHTML(extraClass, label) { var html = "
    "; html += "
    "; - html += label + "
    "; + html += ""; return html; } @@ -24,10 +24,14 @@ $(document).ready(function(){ }); } - function uploadNextChunk(file, offset) { + function uploadNextChunk(file, offset, id) { if (offset == undefined) { offset = 0; } + if (id == undefined) { + // Identify this upload session + id = Math.round(Math.random() * 2147483647); + } var fileSize = file.size; var filename = file.name; @@ -45,6 +49,7 @@ $(document).ready(function(){ url: '/content/upload', type: 'POST', timeout: 30000, // 30 s + headers: {"X-Session-Id": id}, cache: false, processData: false, contentType: false, @@ -64,7 +69,7 @@ $(document).ready(function(){ if (!isFinal) { ajaxObject.done(function (data, textStatus, jqXHR) - { uploadNextChunk(file, offset + CHUNK_SIZE); }); + { uploadNextChunk(file, offset + CHUNK_SIZE, id); }); } else { ajaxObject.done(function(data, textStatus, jqXHR) { isRestoring = true; @@ -210,7 +215,7 @@ $(document).ready(function(){ function updateProgressBars($progressBar, value) { $progressBar.attr('aria-valuenow', value).attr('style', 'width: ' + value + '%'); - $progressBar.find('.ongoing-msg').html(" " + Math.round(value) + "% Complete"); + $progressBar.find('.ongoing-msg').html(" " + Math.round(value) + "%"); } function reloadBackupInformation() { diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 20ca5c94d4..2a5ada729c 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -2519,18 +2519,21 @@ bool DomainServer::handleHTTPSRequest(HTTPSConnection* connection, const QUrl &u } bool DomainServer::processPendingContent(HTTPConnection* connection, QString itemName, QString filename, QByteArray dataChunk) { + static const QString UPLOAD_SESSION_KEY { "X-Session-Id" }; + QByteArray sessionIdBytes = connection->requestHeader(UPLOAD_SESSION_KEY); + int sessionId = sessionIdBytes.toInt(); + if (filename.endsWith(".zip", Qt::CaseInsensitive)) { static const QString TEMPORARY_CONTENT_FILEPATH { QDir::tempPath() + "/hifiUploadContent_XXXXXX.zip" }; - const auto peerAddressHash = qHash(connection->socket()->peerAddress()); - if (_pendingContentFiles.find(peerAddressHash) == _pendingContentFiles.end()) { + if (_pendingContentFiles.find(sessionId) == _pendingContentFiles.end()) { std::unique_ptr newTemp(new QTemporaryFile(TEMPORARY_CONTENT_FILEPATH)); - _pendingContentFiles[peerAddressHash] = std::move(newTemp); + _pendingContentFiles[sessionId] = std::move(newTemp); } - QTemporaryFile& _pendingFileContent = *_pendingContentFiles[peerAddressHash]; + QTemporaryFile& _pendingFileContent = *_pendingContentFiles[sessionId]; if (!_pendingFileContent.open()) { - _pendingContentFiles.erase(peerAddressHash); + _pendingContentFiles.erase(sessionId); connection->respond(HTTPConnection::StatusCode400); return false; } @@ -2543,16 +2546,15 @@ bool DomainServer::processPendingContent(HTTPConnection* connection, QString ite if (itemName == "restore-file-chunk-final" || itemName == "restore-file") { auto deferred = makePromise("recoverFromUploadedBackup"); - deferred->then([this, peerAddressHash](QString error, QVariantMap result) { - _pendingContentFiles.erase(peerAddressHash); + deferred->then([this, sessionId](QString error, QVariantMap result) { + _pendingContentFiles.erase(sessionId); }); _contentManager->recoverFromUploadedFile(deferred, _pendingFileContent.fileName()); } } else if (filename.endsWith(".json", Qt::CaseInsensitive) || filename.endsWith(".json.gz", Qt::CaseInsensitive)) { - auto peerAddressHash = qHash(connection->socket()->peerAddress()); - QByteArray& _pendingUploadedContent = _pendingUploadedContents[peerAddressHash]; + QByteArray& _pendingUploadedContent = _pendingUploadedContents[sessionId]; _pendingUploadedContent += dataChunk; connection->respond(HTTPConnection::StatusCode200); @@ -2560,7 +2562,7 @@ bool DomainServer::processPendingContent(HTTPConnection* connection, QString ite // invoke our method to hand the new octree file off to the octree server QMetaObject::invokeMethod(this, "handleOctreeFileReplacement", Qt::QueuedConnection, Q_ARG(QByteArray, _pendingUploadedContent)); - _pendingUploadedContents.erase(peerAddressHash); + _pendingUploadedContents.erase(sessionId); } } else { connection->respond(HTTPConnection::StatusCode400); diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index 5d6cd4e5f9..f0c20241a2 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -284,8 +284,8 @@ private: QHash> _pendingOAuthConnections; - std::unordered_map _pendingUploadedContents; - std::unordered_map> _pendingContentFiles; + std::unordered_map _pendingUploadedContents; + std::unordered_map> _pendingContentFiles; QThread _assetClientThread; }; From 2a7e22bf317698f2d5d94ce011b8b07c11ef6b43 Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Mon, 19 Nov 2018 15:26:08 -0800 Subject: [PATCH 11/11] Identify initial chunk of an upload so as to be more resilient --- .../resources/web/content/js/content.js | 10 ++++++++-- domain-server/src/DomainServer.cpp | 16 ++++++++++++++-- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/domain-server/resources/web/content/js/content.js b/domain-server/resources/web/content/js/content.js index afee2cc8a9..85bd9e68b3 100644 --- a/domain-server/resources/web/content/js/content.js +++ b/domain-server/resources/web/content/js/content.js @@ -43,7 +43,13 @@ $(document).ready(function(){ var chunk = file.slice(offset, offset + nextChunkSize, file.type); var chunkFormData = new FormData(); - var formItemName = isFinal ? 'restore-file-chunk-final' : 'restore-file-chunk'; + var formItemName = 'restore-file-chunk'; + if (offset == 0) { + formItemName = isFinal ? 'restore-file-chunk-only' : 'restore-file-chunk-initial'; + } else if (isFinal) { + formItemName = 'restore-file-chunk-final'; + } + chunkFormData.append(formItemName, chunk, filename); var ajaxParams = { url: '/content/upload', @@ -57,7 +63,7 @@ $(document).ready(function(){ }; var ajaxObject = $.ajax(ajaxParams); - ajaxObject.fail(function(jqXHR, textStatus, errorThrown) { + ajaxObject.fail(function (jqXHR, textStatus, errorThrown) { showErrorMessage( "Error", "There was a problem restoring domain content.\n" diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 2a5ada729c..2d1abc8c71 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -2523,12 +2523,20 @@ bool DomainServer::processPendingContent(HTTPConnection* connection, QString ite QByteArray sessionIdBytes = connection->requestHeader(UPLOAD_SESSION_KEY); int sessionId = sessionIdBytes.toInt(); + bool newUpload = itemName == "restore-file" || itemName == "restore-file-chunk-initial" || itemName == "restore-file-chunk-only"; + if (filename.endsWith(".zip", Qt::CaseInsensitive)) { static const QString TEMPORARY_CONTENT_FILEPATH { QDir::tempPath() + "/hifiUploadContent_XXXXXX.zip" }; if (_pendingContentFiles.find(sessionId) == _pendingContentFiles.end()) { + if (!newUpload) { + return false; + } std::unique_ptr newTemp(new QTemporaryFile(TEMPORARY_CONTENT_FILEPATH)); _pendingContentFiles[sessionId] = std::move(newTemp); + } else if (newUpload) { + qCDebug(domain_server) << "New upload received using existing session ID"; + _pendingContentFiles[sessionId]->resize(0); } QTemporaryFile& _pendingFileContent = *_pendingContentFiles[sessionId]; @@ -2543,7 +2551,7 @@ bool DomainServer::processPendingContent(HTTPConnection* connection, QString ite // Respond immediately - will timeout if we wait for restore. connection->respond(HTTPConnection::StatusCode200); - if (itemName == "restore-file-chunk-final" || itemName == "restore-file") { + if (itemName == "restore-file" || itemName == "restore-file-chunk-final" || itemName == "restore-file-chunk-only") { auto deferred = makePromise("recoverFromUploadedBackup"); deferred->then([this, sessionId](QString error, QVariantMap result) { @@ -2554,11 +2562,15 @@ bool DomainServer::processPendingContent(HTTPConnection* connection, QString ite } } else if (filename.endsWith(".json", Qt::CaseInsensitive) || filename.endsWith(".json.gz", Qt::CaseInsensitive)) { + if (_pendingUploadedContents.find(sessionId) == _pendingUploadedContents.end() && !newUpload) { + qCDebug(domain_server) << "Json upload with invalid session ID received"; + return false; + } QByteArray& _pendingUploadedContent = _pendingUploadedContents[sessionId]; _pendingUploadedContent += dataChunk; connection->respond(HTTPConnection::StatusCode200); - if (itemName == "restore-file-chunk-final" || itemName == "restore-file") { + if (itemName == "restore-file" || itemName == "restore-file-chunk-final" || itemName == "restore-file-chunk-only") { // invoke our method to hand the new octree file off to the octree server QMetaObject::invokeMethod(this, "handleOctreeFileReplacement", Qt::QueuedConnection, Q_ARG(QByteArray, _pendingUploadedContent));