// // ModelUploader.cpp // interface/src // // Created by Clément Brisset on 3/4/14. // Copyright 2014 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "Application.h" #include "ModelUploader.h" static const QString NAME_FIELD = "name"; static const QString FILENAME_FIELD = "filename"; static const QString TEXDIR_FIELD = "texdir"; static const QString LOD_FIELD = "lod"; static const QString JOINT_INDEX_FIELD = "jointIndex"; static const QString SCALE_FIELD = "scale"; static const QString JOINT_FIELD = "joint"; static const QString FREE_JOINT_FIELD = "freeJoint"; static const QString S3_URL = "http://highfidelity-public.s3-us-west-1.amazonaws.com"; static const QString DATA_SERVER_URL = "https://data-web.highfidelity.io"; static const QString MODEL_URL = "/api/v1/models"; static const QString SETTING_NAME = "LastModelUploadLocation"; static const int MAX_SIZE = 10 * 1024 * 1024; // 10 MB static const int TIMEOUT = 1000; static const int MAX_CHECK = 30; static const int QCOMPRESS_HEADER_POSITION = 0; static const int QCOMPRESS_HEADER_SIZE = 4; ModelUploader::ModelUploader(bool isHead) : _lodCount(-1), _texturesCount(-1), _totalSize(0), _isHead(isHead), _readyToSend(false), _dataMultiPart(new QHttpMultiPart(QHttpMultiPart::FormDataType)), _numberOfChecks(MAX_CHECK) { connect(&_timer, SIGNAL(timeout()), SLOT(checkS3())); } ModelUploader::~ModelUploader() { delete _dataMultiPart; } bool ModelUploader::zip() { // File Dialog QSettings* settings = Application::getInstance()->lockSettings(); QString lastLocation = settings->value(SETTING_NAME).toString(); if (lastLocation.isEmpty()) { lastLocation = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation); // Temporary fix to Qt bug: http://stackoverflow.com/questions/16194475 #ifdef __APPLE__ lastLocation.append("/model.fst"); #endif } QString filename = QFileDialog::getOpenFileName(NULL, "Select your model file ...", lastLocation, "Model files (*.fst *.fbx)"); if (filename == "") { // If the user canceled we return. Application::getInstance()->unlockSettings(); return false; } settings->setValue(SETTING_NAME, filename); Application::getInstance()->unlockSettings(); // First we check the FST file (if any) QFile* fst; QVariantHash mapping; QString basePath; QString fbxFile; if (filename.toLower().endsWith(".fst")) { fst = new QFile(filename, this); if (!fst->open(QFile::ReadOnly | QFile::Text)) { QMessageBox::warning(NULL, QString("ModelUploader::zip()"), QString("Could not open FST file."), QMessageBox::Ok); qDebug() << "[Warning] " << QString("Could not open FST file."); return false; } qDebug() << "Reading FST file : " << QFileInfo(*fst).filePath(); mapping = readMapping(fst->readAll()); basePath = QFileInfo(*fst).path(); fbxFile = basePath + "/" + mapping.value(FILENAME_FIELD).toString(); QFileInfo fbxInfo(fbxFile); if (!fbxInfo.exists() || !fbxInfo.isFile()) { // Check existence QMessageBox::warning(NULL, QString("ModelUploader::zip()"), QString("FBX file %1 could not be found.").arg(fbxInfo.fileName()), QMessageBox::Ok); qDebug() << "[Warning] " << QString("FBX file %1 could not be found.").arg(fbxInfo.fileName()); return false; } } else { fst = new QTemporaryFile(this); fbxFile = filename; basePath = QFileInfo(filename).path(); mapping.insert(FILENAME_FIELD, QFileInfo(filename).fileName()); } // open the fbx file QFile fbx(fbxFile); if (!fbx.open(QIODevice::ReadOnly)) { return false; } QByteArray fbxContents = fbx.readAll(); FBXGeometry geometry = readFBX(fbxContents, QVariantHash()); // make sure we have some basic mappings if (!mapping.contains(NAME_FIELD)) { mapping.insert(NAME_FIELD, QFileInfo(filename).baseName()); } if (!mapping.contains(TEXDIR_FIELD)) { mapping.insert(TEXDIR_FIELD, "."); } // open the dialog to configure the rest ModelPropertiesDialog properties(_isHead, mapping, basePath, geometry); if (properties.exec() == QDialog::Rejected) { return false; } mapping = properties.getMapping(); QByteArray nameField = mapping.value(NAME_FIELD).toByteArray(); if (!nameField.isEmpty()) { QHttpPart textPart; textPart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data; name=\"model_name\""); textPart.setBody(nameField); _dataMultiPart->append(textPart); _url = S3_URL + ((_isHead)? "/models/heads/" : "/models/skeletons/") + nameField + ".fst"; } else { QMessageBox::warning(NULL, QString("ModelUploader::zip()"), QString("Model name is missing in the .fst file."), QMessageBox::Ok); qDebug() << "[Warning] " << QString("Model name is missing in the .fst file."); return false; } QByteArray texdirField = mapping.value(TEXDIR_FIELD).toByteArray(); QString texDir; if (!texdirField.isEmpty()) { texDir = basePath + "/" + texdirField; QFileInfo texInfo(texDir); if (!texInfo.exists() || !texInfo.isDir()) { QMessageBox::warning(NULL, QString("ModelUploader::zip()"), QString("Texture directory could not be found."), QMessageBox::Ok); qDebug() << "[Warning] " << QString("Texture directory could not be found."); return false; } } QVariantHash lodField = mapping.value(LOD_FIELD).toHash(); for (QVariantHash::const_iterator it = lodField.constBegin(); it != lodField.constEnd(); it++) { QFileInfo lod(basePath + "/" + it.key()); if (!lod.exists() || !lod.isFile()) { // Check existence QMessageBox::warning(NULL, QString("ModelUploader::zip()"), QString("LOD file %1 could not be found.").arg(lod.fileName()), QMessageBox::Ok); qDebug() << "[Warning] " << QString("FBX file %1 could not be found.").arg(lod.fileName()); } // Compress and copy if (!addPart(lod.filePath(), QString("lod%1").arg(++_lodCount))) { return false; } } // Write out, compress and copy the fst if (!addPart(*fst, writeMapping(mapping), QString("fst"))) { return false; } // Compress and copy the fbx if (!addPart(fbx, fbxContents, "fbx")) { return false; } if (!addTextures(texDir, geometry)) { return false; } QHttpPart textPart; textPart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data;" " name=\"model_category\""); if (_isHead) { textPart.setBody("heads"); } else { textPart.setBody("skeletons"); } _dataMultiPart->append(textPart); qDebug() << writeMapping(mapping); return false; _readyToSend = true; return true; } void ModelUploader::send() { if (!zip()) { deleteLater(); return; } JSONCallbackParameters callbackParams; callbackParams.jsonCallbackReceiver = this; callbackParams.jsonCallbackMethod = "checkJSON"; callbackParams.errorCallbackReceiver = this; callbackParams.errorCallbackMethod = "uploadFailed"; AccountManager::getInstance().authenticatedRequest(MODEL_URL + "/" + QFileInfo(_url).baseName(), QNetworkAccessManager::GetOperation, callbackParams); qDebug() << "Sending model..."; _progressDialog = new QDialog(); _progressBar = new QProgressBar(_progressDialog); _progressBar->setRange(0, 100); _progressBar->setValue(0); _progressDialog->setWindowTitle("Uploading model..."); _progressDialog->setLayout(new QGridLayout(_progressDialog)); _progressDialog->layout()->addWidget(_progressBar); _progressDialog->exec(); delete _progressDialog; _progressDialog = NULL; _progressBar = NULL; } void ModelUploader::checkJSON(const QJsonObject& jsonResponse) { if (jsonResponse.contains("status") && jsonResponse.value("status").toString() == "success") { qDebug() << "status : success"; JSONCallbackParameters callbackParams; callbackParams.jsonCallbackReceiver = this; callbackParams.jsonCallbackMethod = "uploadSuccess"; callbackParams.errorCallbackReceiver = this; callbackParams.errorCallbackMethod = "uploadFailed"; callbackParams.updateReciever = this; callbackParams.updateSlot = SLOT(uploadUpdate(qint64, qint64)); if (jsonResponse.contains("exists") && jsonResponse.value("exists").toBool()) { qDebug() << "exists : true"; if (jsonResponse.contains("can_update") && jsonResponse.value("can_update").toBool()) { qDebug() << "can_update : true"; AccountManager::getInstance().authenticatedRequest(MODEL_URL + "/" + QFileInfo(_url).baseName(), QNetworkAccessManager::PutOperation, callbackParams, QByteArray(), _dataMultiPart); _dataMultiPart = NULL; } else { qDebug() << "can_update : false"; if (_progressDialog) { _progressDialog->reject(); } QMessageBox::warning(NULL, QString("ModelUploader::checkJSON()"), QString("This model already exist and is own by someone else."), QMessageBox::Ok); deleteLater(); } } else { qDebug() << "exists : false"; AccountManager::getInstance().authenticatedRequest(MODEL_URL, QNetworkAccessManager::PostOperation, callbackParams, QByteArray(), _dataMultiPart); _dataMultiPart = NULL; } } else { qDebug() << "status : failed"; if (_progressDialog) { _progressDialog->reject(); } QMessageBox::warning(NULL, QString("ModelUploader::checkJSON()"), QString("Something went wrong with the data-server."), QMessageBox::Ok); deleteLater(); } } void ModelUploader::uploadUpdate(qint64 bytesSent, qint64 bytesTotal) { if (_progressDialog) { _progressBar->setRange(0, bytesTotal); _progressBar->setValue(bytesSent); } } void ModelUploader::uploadSuccess(const QJsonObject& jsonResponse) { if (_progressDialog) { _progressDialog->accept(); } QMessageBox::information(NULL, QString("ModelUploader::uploadSuccess()"), QString("We are reading your model information."), QMessageBox::Ok); qDebug() << "Model sent with success"; checkS3(); } void ModelUploader::uploadFailed(QNetworkReply::NetworkError errorCode, const QString& errorString) { if (_progressDialog) { _progressDialog->reject(); } qDebug() << "Model upload failed (" << errorCode << "): " << errorString; QMessageBox::warning(NULL, QString("ModelUploader::uploadFailed()"), QString("There was a problem with your upload, please try again later."), QMessageBox::Ok); deleteLater(); } void ModelUploader::checkS3() { qDebug() << "Checking S3 for " << _url; QNetworkRequest request(_url); QNetworkReply* reply = _networkAccessManager.head(request); connect(reply, SIGNAL(finished()), SLOT(processCheck())); } void ModelUploader::processCheck() { QNetworkReply* reply = static_cast(sender()); _timer.stop(); switch (reply->error()) { case QNetworkReply::NoError: QMessageBox::information(NULL, QString("ModelUploader::processCheck()"), QString("Your model is now available in the browser."), QMessageBox::Ok); deleteLater(); break; case QNetworkReply::ContentNotFoundError: if (--_numberOfChecks) { _timer.start(TIMEOUT); break; } default: QMessageBox::warning(NULL, QString("ModelUploader::processCheck()"), QString("We could not verify that your model was sent sucessfully\n" "but it may have. If you do not see it in the model browser, try to upload again."), QMessageBox::Ok); deleteLater(); break; } delete reply; } bool ModelUploader::addTextures(const QString& texdir, const FBXGeometry& geometry) { foreach (FBXMesh mesh, geometry.meshes) { foreach (FBXMeshPart part, mesh.parts) { if (!part.diffuseTexture.filename.isEmpty() && part.diffuseTexture.content.isEmpty()) { if (!addPart(texdir + "/" + part.diffuseTexture.filename, QString("texture%1").arg(++_texturesCount))) { return false; } } if (!part.normalTexture.filename.isEmpty() && part.normalTexture.content.isEmpty()) { if (!addPart(texdir + "/" + part.normalTexture.filename, QString("texture%1").arg(++_texturesCount))) { return false; } } } } return true; } bool ModelUploader::addPart(const QString &path, const QString& name) { QFile file(path); if (!file.open(QIODevice::ReadOnly)) { QMessageBox::warning(NULL, QString("ModelUploader::addPart()"), QString("Could not open %1").arg(path), QMessageBox::Ok); qDebug() << "[Warning] " << QString("Could not open %1").arg(path); return false; } return addPart(file, file.readAll(), name); } bool ModelUploader::addPart(const QFile& file, const QByteArray& contents, const QString& name) { QByteArray buffer = qCompress(contents); // Qt's qCompress() default compression level (-1) is the standard zLib compression. // Here remove Qt's custom header that prevent the data server from uncompressing the files with zLib. buffer.remove(QCOMPRESS_HEADER_POSITION, QCOMPRESS_HEADER_SIZE); QHttpPart part; part.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data;" " name=\"" + name.toUtf8() + "\";" " filename=\"" + QFileInfo(file).fileName().toUtf8() + "\"")); part.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("application/octet-stream")); part.setBody(buffer); _dataMultiPart->append(part); qDebug() << "File " << QFileInfo(file).fileName() << " added to model."; _totalSize += file.size(); if (_totalSize > MAX_SIZE) { QMessageBox::warning(NULL, QString("ModelUploader::zip()"), QString("Model too big, over %1 Bytes.").arg(MAX_SIZE), QMessageBox::Ok); qDebug() << "[Warning] " << QString("Model too big, over %1 Bytes.").arg(MAX_SIZE); return false; } qDebug() << "Current model size: " << _totalSize; return true; } ModelPropertiesDialog::ModelPropertiesDialog(bool isHead, const QVariantHash& originalMapping, const QString& basePath, const FBXGeometry& geometry) : _isHead(isHead), _originalMapping(originalMapping), _basePath(basePath), _geometry(geometry) { setWindowTitle("Set Model Properties"); QFormLayout* form = new QFormLayout(); setLayout(form); form->addRow("Name:", _name = new QLineEdit()); form->addRow("Texture Directory:", _textureDirectory = new QPushButton()); connect(_textureDirectory, SIGNAL(clicked(bool)), SLOT(chooseTextureDirectory())); form->addRow("Scale:", _scale = new QDoubleSpinBox()); _scale->setMaximum(FLT_MAX); _scale->setSingleStep(0.01); if (isHead) { form->addRow("Left Eye Joint:", _leftEyeJoint = createJointBox()); form->addRow("Right Eye Joint:", _rightEyeJoint = createJointBox()); } form->addRow("Neck Joint:", _neckJoint = createJointBox()); if (!isHead) { form->addRow("Root Joint:", _rootJoint = createJointBox()); form->addRow("Lean Joint:", _leanJoint = createJointBox()); form->addRow("Head Joint:", _headJoint = createJointBox()); form->addRow("Left Hand Joint:", _leftHandJoint = createJointBox()); form->addRow("Right Hand Joint:", _rightHandJoint = createJointBox()); form->addRow("Free Joints:", _freeJoints = new QVBoxLayout()); QPushButton* newFreeJoint = new QPushButton("New Free Joint"); _freeJoints->addWidget(newFreeJoint); connect(newFreeJoint, SIGNAL(clicked(bool)), SLOT(createNewFreeJoint())); } QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel | QDialogButtonBox::Reset); connect(buttons, SIGNAL(accepted()), SLOT(accept())); connect(buttons, SIGNAL(rejected()), SLOT(reject())); connect(buttons->button(QDialogButtonBox::Reset), SIGNAL(clicked(bool)), SLOT(reset())); form->addRow(buttons); // reset to initialize the fields reset(); } QVariantHash ModelPropertiesDialog::getMapping() const { QVariantHash mapping = _originalMapping; mapping.insert(NAME_FIELD, _name->text()); mapping.insert(TEXDIR_FIELD, _textureDirectory->text()); mapping.insert(SCALE_FIELD, QString::number(_scale->value())); // update the joint indices QVariantHash jointIndices; for (int i = 0; i < _geometry.joints.size(); i++) { jointIndices.insert(_geometry.joints.at(i).name, QString::number(i)); } mapping.insert(JOINT_INDEX_FIELD, jointIndices); QVariantHash joints = mapping.value(JOINT_FIELD).toHash(); if (_isHead) { joints.insert("jointEyeLeft", _leftEyeJoint->currentText()); joints.insert("jointEyeRight", _rightEyeJoint->currentText()); } joints.insert("jointNeck", _neckJoint->currentText()); if (!_isHead) { joints.insert("jointRoot", _rootJoint->currentText()); joints.insert("jointLean", _leanJoint->currentText()); joints.insert("jointHead", _headJoint->currentText()); joints.insert("jointLeftHand", _leftHandJoint->currentText()); joints.insert("jointRightHand", _rightHandJoint->currentText()); mapping.remove(FREE_JOINT_FIELD); for (int i = 0; i < _freeJoints->count() - 1; i++) { QComboBox* box = static_cast(_freeJoints->itemAt(i)->widget()->layout()->itemAt(0)->widget()); mapping.insertMulti(FREE_JOINT_FIELD, box->currentText()); } } mapping.insert(JOINT_FIELD, joints); return mapping; } void ModelPropertiesDialog::reset() { _name->setText(_originalMapping.value(NAME_FIELD).toString()); _textureDirectory->setText(_originalMapping.value(TEXDIR_FIELD).toString()); _scale->setValue(_originalMapping.value(SCALE_FIELD, 1.0).toDouble()); QVariantHash jointHash = _originalMapping.value(JOINT_FIELD).toHash(); if (_isHead) { _leftEyeJoint->setCurrentText(jointHash.value("jointEyeLeft").toString()); _rightEyeJoint->setCurrentText(jointHash.value("jointEyeRight").toString()); } _neckJoint->setCurrentText(jointHash.value("jointNeck").toString()); if (!_isHead) { _rootJoint->setCurrentText(jointHash.value("jointRoot").toString()); _leanJoint->setCurrentText(jointHash.value("jointLean").toString()); _headJoint->setCurrentText(jointHash.value("jointHead").toString()); _leftHandJoint->setCurrentText(jointHash.value("jointLeftHand").toString()); _rightHandJoint->setCurrentText(jointHash.value("jointRightHand").toString()); while (_freeJoints->count() > 1) { delete _freeJoints->itemAt(0)->widget(); } foreach (const QVariant& joint, _originalMapping.values(FREE_JOINT_FIELD)) { createNewFreeJoint(joint.toString()); } } } void ModelPropertiesDialog::chooseTextureDirectory() { QString directory = QFileDialog::getExistingDirectory(this, "Choose Texture Directory", _basePath + "/" + _textureDirectory->text()); if (directory.isEmpty()) { return; } if (!directory.startsWith(_basePath)) { QMessageBox::warning(NULL, "Invalid texture directory", "Texture directory must be child of base path."); return; } _textureDirectory->setText(directory.length() == _basePath.length() ? "." : directory.mid(_basePath.length() + 1)); } void ModelPropertiesDialog::createNewFreeJoint(const QString& joint) { QWidget* freeJoint = new QWidget(); QHBoxLayout* freeJointLayout = new QHBoxLayout(); freeJointLayout->setContentsMargins(QMargins()); freeJoint->setLayout(freeJointLayout); QComboBox* jointBox = createJointBox(); jointBox->setCurrentText(joint); freeJointLayout->addWidget(jointBox, 1); QPushButton* deleteJoint = new QPushButton("Delete"); freeJointLayout->addWidget(deleteJoint); freeJoint->connect(deleteJoint, SIGNAL(clicked(bool)), SLOT(deleteLater())); _freeJoints->insertWidget(_freeJoints->count() - 1, freeJoint); } QComboBox* ModelPropertiesDialog::createJointBox() const { QComboBox* box = new QComboBox(); foreach (const FBXJoint& joint, _geometry.joints) { box->addItem(joint.name); } return box; }