From dcf20d2a15a674ec35b54e58e42eb1816b390689 Mon Sep 17 00:00:00 2001 From: Alexander Ivash Date: Fri, 26 Oct 2018 00:37:44 +0300 Subject: [PATCH 01/15] FB17381 - Reticle.setVisible isn't working --- scripts/system/controllers/controllerModules/mouseHMD.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/system/controllers/controllerModules/mouseHMD.js b/scripts/system/controllers/controllerModules/mouseHMD.js index 27fe82ca19..172923a8e2 100644 --- a/scripts/system/controllers/controllerModules/mouseHMD.js +++ b/scripts/system/controllers/controllerModules/mouseHMD.js @@ -29,6 +29,7 @@ function MouseHMD() { var _this = this; + this.hmdWasActive = HMD.active; this.mouseMoved = false; this.mouseActivity = new TimeLock(5000); this.handControllerActivity = new TimeLock(4000); @@ -102,6 +103,8 @@ this.isReady = function(controllerData, deltaTime) { var now = Date.now(); + var hmdChanged = this.hmdWasActive !== HMD.active; + this.hmdWasActive = HMD.active; this.triggersPressed(controllerData, now); if (HMD.active) { if (!this.mouseActivity.expired(now) && _this.handControllerActivity.expired()) { @@ -110,7 +113,7 @@ } else { Reticle.visible = false; } - } else if (!Reticle.visible) { + } else if (hmdChanged && !Reticle.visible) { Reticle.visible = true; } From 298c5efe69a1d347b84481f86d83dbf7509f8682 Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Thu, 8 Nov 2018 12:13:56 -0800 Subject: [PATCH 02/15] 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 03/15] 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 04/15] 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 05/15] 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 06/15] 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 07/15] 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 08/15] 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 09/15] 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 10/15] 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 11/15] 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 12/15] 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)); From ff6d43d0e4890d8612bb0d177064ad2e3dfdb5e2 Mon Sep 17 00:00:00 2001 From: David Back Date: Mon, 19 Nov 2018 16:15:11 -0800 Subject: [PATCH 13/15] block updates during dragging --- scripts/system/edit.js | 9 ++ scripts/system/html/js/draggableNumber.js | 57 +++++-- scripts/system/html/js/entityProperties.js | 166 +++++++++++++-------- 3 files changed, 150 insertions(+), 82 deletions(-) diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 48fdb8e565..85fd8c536d 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -2223,6 +2223,7 @@ var PropertiesTool = function (opts) { // are selected or if no entity is selected this will be `null`. var currentSelectedEntityID = null; var statusMonitor = null; + var nextPropertyUpdateDisabled = false; that.setVisible = function (newVisible) { visible = newVisible; @@ -2260,6 +2261,11 @@ var PropertiesTool = function (opts) { }; function updateSelections(selectionUpdated) { + if (nextPropertyUpdateDisabled) { + nextPropertyUpdateDisabled = false; + return; + } + var data = { type: 'update', spaceMode: selectionDisplay.getSpaceMode() @@ -2356,6 +2362,7 @@ var PropertiesTool = function (opts) { } } pushCommandForSelections(); + nextPropertyUpdateDisabled = data.blockUpdateCallback === true; selectionManager._update(false, this); } else if (data.type === 'saveUserData' || data.type === 'saveMaterialData') { //the event bridge and json parsing handle our avatar id string differently. @@ -2466,6 +2473,8 @@ var PropertiesTool = function (opts) { tooltips: Script.require('./assets/data/createAppTooltips.json'), hmdActive: HMD.active, }); + } else if (data.type === "updateProperties") { + updateSelections(true); } }; diff --git a/scripts/system/html/js/draggableNumber.js b/scripts/system/html/js/draggableNumber.js index 1f4bc21441..c08cac2ce4 100644 --- a/scripts/system/html/js/draggableNumber.js +++ b/scripts/system/html/js/draggableNumber.js @@ -8,11 +8,14 @@ const DELTA_X_FOCUS_THRESHOLD = 1; -function DraggableNumber(min, max, step, decimals) { +function DraggableNumber(min, max, step, decimals, dragStart, dragEnd) { this.min = min; this.max = max; this.step = step !== undefined ? step : 1; this.decimals = decimals; + this.dragStartFunction = dragStart; + this.dragEndFunction = dragEnd; + this.dragging = false; this.initialMouseEvent = null; this.lastMouseEvent = null; this.valueChangeFunction = null; @@ -32,7 +35,7 @@ DraggableNumber.prototype = { mouseUp: function(event) { if (event.target === this.elText && this.initialMouseEvent) { let dx = event.clientX - this.initialMouseEvent.clientX; - if (dx <= DELTA_X_FOCUS_THRESHOLD) { + if (Math.abs(dx) <= DELTA_X_FOCUS_THRESHOLD) { this.elInput.style.visibility = "visible"; this.elText.style.visibility = "hidden"; } @@ -41,22 +44,32 @@ DraggableNumber.prototype = { }, documentMouseMove: function(event) { - if (this.lastMouseEvent) { - let initialValue = this.elInput.value; - let dx = event.clientX - this.lastMouseEvent.clientX; - let changeValue = dx !== 0; - if (changeValue) { - while (dx !== 0) { - if (dx > 0) { - this.stepUp(); - --dx; - } else { - this.stepDown(); - ++dx; + if (this.initialMouseEvent) { + let dxFromInitial = event.clientX - this.initialMouseEvent.clientX; + if (Math.abs(dxFromInitial) > DELTA_X_FOCUS_THRESHOLD && this.lastMouseEvent) { + let initialValue = this.elInput.value; + let dx = event.clientX - this.lastMouseEvent.clientX; + let changeValue = dx !== 0; + if (changeValue) { + while (dx !== 0) { + if (dx > 0) { + this.elInput.stepUp(); + --dx; + } else { + this.elInput.stepDown(); + ++dx; + } + } + this.inputChange(); + if (this.valueChangeFunction) { + this.valueChangeFunction(); } } - if (this.valueChangeFunction) { - this.valueChangeFunction(); + if (!this.dragging) { + if (this.dragStartFunction) { + this.dragStartFunction(); + } + this.dragging = true; } } this.lastMouseEvent = event; @@ -64,6 +77,12 @@ DraggableNumber.prototype = { }, documentMouseUp: function(event) { + if (this.dragging) { + if (this.dragEndFunction) { + this.dragEndFunction(); + } + this.dragging = false; + } this.lastMouseEvent = null; document.removeEventListener("mousemove", this.onDocumentMouseMove); document.removeEventListener("mouseup", this.onDocumentMouseUp); @@ -72,11 +91,17 @@ DraggableNumber.prototype = { stepUp: function() { this.elInput.stepUp(); this.inputChange(); + if (this.valueChangeFunction) { + this.valueChangeFunction(); + } }, stepDown: function() { this.elInput.stepDown(); this.inputChange(); + if (this.valueChangeFunction) { + this.valueChangeFunction(); + } }, setValue: function(newValue) { diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index c49e0d88f6..850c012be0 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -1636,7 +1636,7 @@ function updateVisibleSpaceModeProperties() { * PROPERTY UPDATE FUNCTIONS */ -function updateProperty(originalPropertyName, propertyValue, isParticleProperty) { +function updateProperty(originalPropertyName, propertyValue, isParticleProperty, blockUpdateCallback) { let propertyUpdate = {}; // if this is a compound property name (i.e. animation.running) then split it by . up to 3 times let splitPropertyName = originalPropertyName.split('.'); @@ -1662,7 +1662,7 @@ function updateProperty(originalPropertyName, propertyValue, isParticleProperty) }); particleSyncDebounce(); } else { - updateProperties(propertyUpdate); + updateProperties(propertyUpdate, blockUpdateCallback); } } @@ -1671,66 +1671,89 @@ var particleSyncDebounce = _.debounce(function () { particlePropertyUpdates = {}; }, DEBOUNCE_TIMEOUT); -function updateProperties(propertiesToUpdate) { +function updateProperties(propertiesToUpdate, blockUpdateCallback) { + if (blockUpdateCallback === undefined) { + blockUpdateCallback = false; + } EventBridge.emitWebEvent(JSON.stringify({ id: lastEntityID, type: "update", - properties: propertiesToUpdate + properties: propertiesToUpdate, + blockUpdateCallback: blockUpdateCallback })); } -function createEmitTextPropertyUpdateFunction(propertyName, isParticleProperty) { +function createEmitTextPropertyUpdateFunction(property) { return function() { - updateProperty(propertyName, this.value, isParticleProperty); + updateProperty(property.name, this.value, property.isParticleProperty); }; } -function createEmitCheckedPropertyUpdateFunction(propertyName, inverse, isParticleProperty) { +function createEmitCheckedPropertyUpdateFunction(property) { return function() { - updateProperty(propertyName, inverse ? !this.checked : this.checked, isParticleProperty); + updateProperty(property.name, property.data.inverse ? !this.checked : this.checked, property.isParticleProperty); }; } -function createEmitNumberPropertyUpdateFunction(propertyName, multiplier, isParticleProperty) { +function createDragStartFunction(property) { return function() { + property.dragging = true; + }; +} + +function createDragEndFunction(property) { + return function() { + property.dragging = false; + EventBridge.emitWebEvent(JSON.stringify({ + type: "updateProperties" + })); + }; +} + +function createEmitNumberPropertyUpdateFunction(property) { + return function() { + let multiplier = property.data.multiplier; if (multiplier === undefined) { multiplier = 1; } let value = parseFloat(this.value) * multiplier; - updateProperty(propertyName, value, isParticleProperty); + updateProperty(property.name, value, property.isParticleProperty, property.dragging); }; } -function createEmitVec2PropertyUpdateFunction(propertyName, elX, elY, multiplier, isParticleProperty) { +function createEmitVec2PropertyUpdateFunction(property) { return function () { + let multiplier = property.data.multiplier; if (multiplier === undefined) { multiplier = 1; } let newValue = { - x: elX.value * multiplier, - y: elY.value * multiplier + x: property.elNumberX.elInput.value * multiplier, + y: property.elNumberY.elInput.value * multiplier }; - updateProperty(propertyName, newValue, isParticleProperty); + updateProperty(property.name, newValue, property.isParticleProperty, property.dragging); }; } -function createEmitVec3PropertyUpdateFunction(propertyName, elX, elY, elZ, multiplier, isParticleProperty) { +function createEmitVec3PropertyUpdateFunction(property) { return function() { + let multiplier = property.data.multiplier; if (multiplier === undefined) { multiplier = 1; } let newValue = { - x: elX.value * multiplier, - y: elY.value * multiplier, - z: elZ.value * multiplier + x: property.elNumberX.elInput.value * multiplier, + y: property.elNumberY.elInput.value * multiplier, + z: property.elNumberZ.elInput.value * multiplier }; - updateProperty(propertyName, newValue, isParticleProperty); + updateProperty(property.name, newValue, property.isParticleProperty, property.dragging); }; } -function createEmitColorPropertyUpdateFunction(propertyName, elRed, elGreen, elBlue, isParticleProperty) { +function createEmitColorPropertyUpdateFunction(property) { return function() { - emitColorPropertyUpdate(propertyName, elRed.value, elGreen.value, elBlue.value, isParticleProperty); + emitColorPropertyUpdate(property.name, property.elNumberR.elInput.value, property.elNumberG.elInput.value, + property.elNumberB.elInput.value, property.isParticleProperty); }; } @@ -1755,10 +1778,10 @@ function updateCheckedSubProperty(propertyName, propertyValue, subPropertyElemen updateProperty(propertyName, propertyValue, isParticleProperty); } -function createImageURLUpdateFunction(propertyName, isParticleProperty) { +function createImageURLUpdateFunction(property) { return function () { let newTextures = JSON.stringify({ "tex.picture": this.value }); - updateProperty(propertyName, newTextures, isParticleProperty); + updateProperty(property.name, newTextures, property.isParticleProperty); }; } @@ -1768,7 +1791,6 @@ function createImageURLUpdateFunction(propertyName, isParticleProperty) { */ function createStringProperty(property, elProperty) { - let propertyName = property.name; let elementID = property.elementID; let propertyData = property.data; @@ -1782,7 +1804,7 @@ function createStringProperty(property, elProperty) { `) - elInput.addEventListener('change', createEmitTextPropertyUpdateFunction(propertyName, property.isParticleProperty)); + elInput.addEventListener('change', createEmitTextPropertyUpdateFunction(property)); elProperty.appendChild(elInput); @@ -1821,30 +1843,30 @@ function createBoolProperty(property, elProperty) { elInput, propertyName, property.isParticleProperty); }); } else { - elInput.addEventListener('change', createEmitCheckedPropertyUpdateFunction(propertyName, propertyData.inverse, - property.isParticleProperty)); + elInput.addEventListener('change', createEmitCheckedPropertyUpdateFunction(property)); } return elInput; } function createNumberProperty(property, elProperty) { - let propertyName = property.name; let elementID = property.elementID; let propertyData = property.data; elProperty.className = "draggable-number"; - let elDraggableNumber = new DraggableNumber(propertyData.min, propertyData.max, - propertyData.step, propertyData.decimals); + let dragStartFunction = createDragStartFunction(property); + let dragEndFunction = createDragEndFunction(property); + + let elDraggableNumber = new DraggableNumber(propertyData.min, propertyData.max, propertyData.step, + propertyData.decimals, dragStartFunction, dragEndFunction); let defaultValue = propertyData.defaultValue; if (defaultValue !== undefined) { elDraggableNumber.elInput.value = defaultValue; } - let valueChangeFunction = createEmitNumberPropertyUpdateFunction(propertyName, propertyData.multiplier, - property.isParticleProperty); + let valueChangeFunction = createEmitNumberPropertyUpdateFunction(property); elDraggableNumber.setValueChangeFunction(valueChangeFunction); elDraggableNumber.elInput.setAttribute("id", elementID); @@ -1858,22 +1880,18 @@ function createNumberProperty(property, elProperty) { } function createVec3Property(property, elProperty) { - let propertyName = property.name; - let elementID = property.elementID; let propertyData = property.data; elProperty.className = propertyData.vec3Type + " fstuple"; - let elNumberX = createTupleNumberInput(elProperty, elementID, propertyData.subLabels[VECTOR_ELEMENTS.X_NUMBER], - propertyData.min, propertyData.max, propertyData.step, propertyData.decimals); - let elNumberY = createTupleNumberInput(elProperty, elementID, propertyData.subLabels[VECTOR_ELEMENTS.Y_NUMBER], - propertyData.min, propertyData.max, propertyData.step, propertyData.decimals); - let elNumberZ = createTupleNumberInput(elProperty, elementID, propertyData.subLabels[VECTOR_ELEMENTS.Z_NUMBER], - propertyData.min, propertyData.max, propertyData.step, propertyData.decimals); + let elNumberX = createTupleNumberInput(property, propertyData.subLabels[VECTOR_ELEMENTS.X_NUMBER]); + let elNumberY = createTupleNumberInput(property, propertyData.subLabels[VECTOR_ELEMENTS.Y_NUMBER]); + let elNumberZ = createTupleNumberInput(property, propertyData.subLabels[VECTOR_ELEMENTS.Z_NUMBER]); + elProperty.appendChild(elNumberX.elDiv); + elProperty.appendChild(elNumberY.elDiv); + elProperty.appendChild(elNumberZ.elDiv); - let valueChangeFunction = createEmitVec3PropertyUpdateFunction(propertyName, elNumberX.elInput, elNumberY.elInput, - elNumberZ.elInput, propertyData.multiplier, - property.isParticleProperty); + let valueChangeFunction = createEmitVec3PropertyUpdateFunction(property); elNumberX.setValueChangeFunction(valueChangeFunction); elNumberY.setValueChangeFunction(valueChangeFunction); elNumberZ.setValueChangeFunction(valueChangeFunction); @@ -1886,8 +1904,6 @@ function createVec3Property(property, elProperty) { } function createVec2Property(property, elProperty) { - let propertyName = property.name; - let elementID = property.elementID; let propertyData = property.data; elProperty.className = propertyData.vec2Type + " fstuple"; @@ -1897,13 +1913,15 @@ function createVec2Property(property, elProperty) { elProperty.appendChild(elTuple); - let elNumberX = createTupleNumberInput(elProperty, elementID, propertyData.subLabels[VECTOR_ELEMENTS.X_NUMBER], - propertyData.min, propertyData.max, propertyData.step, propertyData.decimals); - let elNumberY = createTupleNumberInput(elProperty, elementID, propertyData.subLabels[VECTOR_ELEMENTS.Y_NUMBER], - propertyData.min, propertyData.max, propertyData.step, propertyData.decimals); + let dragStartFunction = createDragStartFunction(property); + let dragEndFunction = createDragEndFunction(property); - let valueChangeFunction = createEmitVec2PropertyUpdateFunction(propertyName, elNumberX.elInput, elNumberY.elInput, - propertyData.multiplier, property.isParticleProperty); + let elNumberX = createTupleNumberInput(property, propertyData.subLabels[VECTOR_ELEMENTS.X_NUMBER]); + let elNumberY = createTupleNumberInput(property, propertyData.subLabels[VECTOR_ELEMENTS.Y_NUMBER]); + elProperty.appendChild(elNumberX.elDiv); + elProperty.appendChild(elNumberY.elDiv); + + let valueChangeFunction = createEmitVec2PropertyUpdateFunction(property); elNumberX.setValueChangeFunction(valueChangeFunction); elNumberY.setValueChangeFunction(valueChangeFunction); @@ -1916,6 +1934,7 @@ function createVec2Property(property, elProperty) { function createColorProperty(property, elProperty) { let propertyName = property.name; let elementID = property.elementID; + let propertyData = property.data; elProperty.className = "rgb fstuple"; @@ -1929,12 +1948,24 @@ function createColorProperty(property, elProperty) { elProperty.appendChild(elColorPicker); elProperty.appendChild(elTuple); - let elNumberR = createTupleNumberInput(elTuple, elementID, "red", COLOR_MIN, COLOR_MAX, COLOR_STEP); - let elNumberG = createTupleNumberInput(elTuple, elementID, "green", COLOR_MIN, COLOR_MAX, COLOR_STEP); - let elNumberB = createTupleNumberInput(elTuple, elementID, "blue", COLOR_MIN, COLOR_MAX, COLOR_STEP); + if (propertyData.min === undefined) { + propertyData.min = COLOR_MIN; + } + if (propertyData.max === undefined) { + propertyData.max = COLOR_MAX; + } + if (propertyData.step === undefined) { + propertyData.step = COLOR_STEP; + } - let valueChangeFunction = createEmitColorPropertyUpdateFunction(propertyName, elNumberR.elInput, elNumberG.elInput, - elNumberB.elInput, property.isParticleProperty); + let elNumberR = createTupleNumberInput(property, "red"); + let elNumberG = createTupleNumberInput(property, "green"); + let elNumberB = createTupleNumberInput(property, "blue"); + elTuple.appendChild(elNumberR.elDiv); + elTuple.appendChild(elNumberG.elDiv); + elTuple.appendChild(elNumberB.elDiv); + + let valueChangeFunction = createEmitColorPropertyUpdateFunction(property); elNumberR.setValueChangeFunction(valueChangeFunction); elNumberG.setValueChangeFunction(valueChangeFunction); elNumberB.setValueChangeFunction(valueChangeFunction); @@ -1973,7 +2004,6 @@ function createColorProperty(property, elProperty) { } function createDropdownProperty(property, propertyID, elProperty) { - let propertyName = property.name; let elementID = property.elementID; let propertyData = property.data; @@ -1990,7 +2020,7 @@ function createDropdownProperty(property, propertyID, elProperty) { elInput.add(option); } - elInput.addEventListener('change', createEmitTextPropertyUpdateFunction(propertyName, property.isParticleProperty)); + elInput.addEventListener('change', createEmitTextPropertyUpdateFunction(property)); elProperty.appendChild(elInput); @@ -1998,7 +2028,6 @@ function createDropdownProperty(property, propertyID, elProperty) { } function createTextareaProperty(property, elProperty) { - let propertyName = property.name; let elementID = property.elementID; let propertyData = property.data; @@ -2010,7 +2039,7 @@ function createTextareaProperty(property, elProperty) { elInput.readOnly = true; } - elInput.addEventListener('change', createEmitTextPropertyUpdateFunction(propertyName, property.isParticleProperty)); + elInput.addEventListener('change', createEmitTextPropertyUpdateFunction(property)); elProperty.appendChild(elInput); @@ -2102,7 +2131,10 @@ function createButtonsProperty(property, elProperty, elLabel) { return elProperty; } -function createTupleNumberInput(elTuple, propertyElementID, subLabel, min, max, step, decimals) { +function createTupleNumberInput(property, subLabel) { + let propertyElementID = property.elementID; + let propertyData = property.data; + let elementID = propertyElementID + "-" + subLabel.toLowerCase(); let elLabel = document.createElement('label'); @@ -2111,11 +2143,13 @@ function createTupleNumberInput(elTuple, propertyElementID, subLabel, min, max, elLabel.setAttribute("for", elementID); elLabel.style.visibility = "visible"; - let elDraggableNumber = new DraggableNumber(min, max, step, decimals); + let dragStartFunction = createDragStartFunction(property); + let dragEndFunction = createDragEndFunction(property); + let elDraggableNumber = new DraggableNumber(propertyData.min, propertyData.max, propertyData.step, + propertyData.decimals, dragStartFunction, dragEndFunction); elDraggableNumber.elInput.setAttribute("id", elementID); elDraggableNumber.elDiv.className += " fstuple"; elDraggableNumber.elText.insertBefore(elLabel, elDraggableNumber.elLeftArrow); - elTuple.appendChild(elDraggableNumber.elDiv); return elDraggableNumber; } @@ -2388,7 +2422,7 @@ function multiDataUpdater(groupName, updateKeyPair, userDataElement, defaults, r userDataElement.value = propertyUpdate.userData; - updateProperties(propertyUpdate); + updateProperties(propertyUpdate, false); } var editor = null; @@ -3306,7 +3340,7 @@ function loaded() { } }); - getPropertyInputElement("image").addEventListener('change', createImageURLUpdateFunction('textures', false)); + getPropertyInputElement("image").addEventListener('change', createImageURLUpdateFunction(properties['textures'])); // Collapsible sections let elCollapsible = document.getElementsByClassName("section-header"); @@ -3401,7 +3435,7 @@ function loaded() { let propertyID = elDropdown.getAttribute("propertyID"); let property = properties[propertyID]; property.elInput = dt; - dt.addEventListener('change', createEmitTextPropertyUpdateFunction(property.name, property.isParticleProperty)); + dt.addEventListener('change', createEmitTextPropertyUpdateFunction(property)); } elDropdowns = document.getElementsByTagName("select"); From 249ebb238923d174481ac11b3c2e139da84152c8 Mon Sep 17 00:00:00 2001 From: David Back Date: Mon, 19 Nov 2018 16:21:52 -0800 Subject: [PATCH 14/15] remove unneeded --- scripts/system/html/js/entityProperties.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index 850c012be0..eca38f099a 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -1857,7 +1857,6 @@ function createNumberProperty(property, elProperty) { let dragStartFunction = createDragStartFunction(property); let dragEndFunction = createDragEndFunction(property); - let elDraggableNumber = new DraggableNumber(propertyData.min, propertyData.max, propertyData.step, propertyData.decimals, dragStartFunction, dragEndFunction); @@ -1913,9 +1912,6 @@ function createVec2Property(property, elProperty) { elProperty.appendChild(elTuple); - let dragStartFunction = createDragStartFunction(property); - let dragEndFunction = createDragEndFunction(property); - let elNumberX = createTupleNumberInput(property, propertyData.subLabels[VECTOR_ELEMENTS.X_NUMBER]); let elNumberY = createTupleNumberInput(property, propertyData.subLabels[VECTOR_ELEMENTS.Y_NUMBER]); elProperty.appendChild(elNumberX.elDiv); @@ -2134,7 +2130,6 @@ function createButtonsProperty(property, elProperty, elLabel) { function createTupleNumberInput(property, subLabel) { let propertyElementID = property.elementID; let propertyData = property.data; - let elementID = propertyElementID + "-" + subLabel.toLowerCase(); let elLabel = document.createElement('label'); From dff4e34e7bc96eb622c0d2b4733e0e85b7cb3878 Mon Sep 17 00:00:00 2001 From: David Back Date: Mon, 19 Nov 2018 16:59:33 -0800 Subject: [PATCH 15/15] CR changes --- scripts/system/edit.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 85fd8c536d..077d50ddde 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -2223,7 +2223,7 @@ var PropertiesTool = function (opts) { // are selected or if no entity is selected this will be `null`. var currentSelectedEntityID = null; var statusMonitor = null; - var nextPropertyUpdateDisabled = false; + var blockPropertyUpdates = false; that.setVisible = function (newVisible) { visible = newVisible; @@ -2261,8 +2261,7 @@ var PropertiesTool = function (opts) { }; function updateSelections(selectionUpdated) { - if (nextPropertyUpdateDisabled) { - nextPropertyUpdateDisabled = false; + if (blockPropertyUpdates) { return; } @@ -2362,8 +2361,9 @@ var PropertiesTool = function (opts) { } } pushCommandForSelections(); - nextPropertyUpdateDisabled = data.blockUpdateCallback === true; + blockPropertyUpdates = data.blockUpdateCallback === true; selectionManager._update(false, this); + blockPropertyUpdates = false; } else if (data.type === 'saveUserData' || data.type === 'saveMaterialData') { //the event bridge and json parsing handle our avatar id string differently. var actualID = data.id.split('"')[1];