Showing the changes to interface/src/ModelUploader.cpp (1 of 2 files). Show all
Unified · Split
| @@ -0,0 +1,342 @@ | ||
| 1 | +// | |
| 2 | +// ModelUploader.cpp | |
| 3 | +// hifi | |
| 4 | +// | |
| 5 | +// Created by Clément Brisset on 3/4/14. | |
| 6 | +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. | |
| 7 | +// | |
| 8 | +// | |
| 9 | + | |
| 10 | +#include <QDebug> | |
| 11 | +#include <QFile> | |
| 12 | +#include <QFileDialog> | |
| 13 | +#include <QGridLayout> | |
| 14 | +#include <QHttpMultiPart> | |
| 15 | +#include <QMessageBox> | |
| 16 | +#include <QProgressBar> | |
| 17 | +#include <QStandardPaths> | |
| 18 | +#include <QTextStream> | |
| 19 | +#include <QVariant> | |
| 20 | + | |
| 21 | +#include <AccountManager.h> | |
| 22 | + | |
| 23 | +#include "renderer/FBXReader.h" | |
| 24 | +#include "ModelUploader.h" | |
| 25 | + | |
| 26 | + | |
| 27 | +static const QString NAME_FIELD = "name"; | |
| 28 | +static const QString FILENAME_FIELD = "filename"; | |
| 29 | +static const QString TEXDIR_FIELD = "texdir"; | |
| 30 | +static const QString LOD_FIELD = "lod"; | |
| 31 | + | |
| 32 | +static const QString S3_URL = "http://highfidelity-public.s3-us-west-1.amazonaws.com"; | |
| 33 | +static const QString MODEL_URL = "/api/v1/models"; | |
| 34 | + | |
| 35 | +static const int MAX_SIZE = 10 * 1024 * 1024; // 10 MB | |
| 36 | +static const int TIMEOUT = 1000; | |
| 37 | +static const int MAX_CHECK = 30; | |
| 38 | + | |
| 39 | +static const int QCOMPRESS_HEADER_POSITION = 0; | |
| 40 | +static const int QCOMPRESS_HEADER_SIZE = 4; | |
| 41 | + | |
| 42 | +ModelUploader::ModelUploader(bool isHead) : | |
| 43 | + _lodCount(-1), | |
| 44 | + _texturesCount(-1), | |
| 45 | + _totalSize(0), | |
| 46 | + _isHead(isHead), | |
| 47 | + _readyToSend(false), | |
| 48 | + _dataMultiPart(new QHttpMultiPart(QHttpMultiPart::FormDataType)), | |
| 49 | + _numberOfChecks(MAX_CHECK) | |
| 50 | +{ | |
| 51 | + connect(&_timer, SIGNAL(timeout()), SLOT(checkS3())); | |
| 52 | +} | |
| 53 | + | |
| 54 | +ModelUploader::~ModelUploader() { | |
| 55 | + delete _dataMultiPart; | |
| 56 | +} | |
| 57 | + | |
| 58 | +bool ModelUploader::zip() { | |
| 59 | + // File Dialog | |
| 60 | + QString filename = QFileDialog::getOpenFileName(NULL, | |
| 61 | + "Select your .fst file ...", | |
| 62 | + QStandardPaths::writableLocation(QStandardPaths::HomeLocation), | |
| 63 | + "*.fst"); | |
| 64 | + if (filename == "") { | |
| 65 | + // If the user canceled we return. | |
| 66 | + return false; | |
| 67 | + } | |
| 68 | + QString texDir; | |
| 69 | + QString fbxFile; | |
| 70 | + | |
| 71 | + | |
| 72 | + // First we check the FST file | |
| 73 | + QFile fst(filename); | |
| 74 | + if (!fst.open(QFile::ReadOnly | QFile::Text)) { | |
| 75 | + QMessageBox::warning(NULL, | |
| 76 | + QString("ModelUploader::zip()"), | |
| 77 | + QString("Could not open FST file."), | |
| 78 | + QMessageBox::Ok); | |
| 79 | + qDebug() << "[Warning] " << QString("Could not open FST file."); | |
| 80 | + return false; | |
| 81 | + } | |
| 82 | + qDebug() << "Reading FST file : " << QFileInfo(fst).filePath(); | |
| 83 | + | |
| 84 | + // Compress and copy the fst | |
| 85 | + if (!addPart(QFileInfo(fst).filePath(), QString("fst"))) { | |
| 86 | + return false; | |
| 87 | + } | |
| 88 | + | |
| 89 | + // Let's read through the FST file | |
| 90 | + QTextStream stream(&fst); | |
| 91 | + QList<QString> line; | |
| 92 | + while (!stream.atEnd()) { | |
| 93 | + line = stream.readLine().split(QRegExp("[ =]"), QString::SkipEmptyParts); | |
| 94 | + if (line.isEmpty()) { | |
| 95 | + continue; | |
| 96 | + } | |
| 97 | + | |
| 98 | + // according to what is read, we modify the command | |
| 99 | + if (line[0] == NAME_FIELD) { | |
| 100 | + QHttpPart textPart; | |
| 101 | + textPart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data;" | |
| 102 | + " name=\"model_name\""); | |
| 103 | + textPart.setBody(line[1].toUtf8()); | |
| 104 | + _dataMultiPart->append(textPart); | |
| 105 | + _url = S3_URL + ((_isHead)? "/models/heads/" : "/models/skeletons/") + line[1].toUtf8() + ".fst"; | |
| 106 | + } else if (line[0] == FILENAME_FIELD) { | |
| 107 | + fbxFile = QFileInfo(fst).path() + "/" + line[1]; | |
| 108 | + QFileInfo fbxInfo(fbxFile); | |
| 109 | + if (!fbxInfo.exists() || !fbxInfo.isFile()) { // Check existence | |
| 110 | + QMessageBox::warning(NULL, | |
| 111 | + QString("ModelUploader::zip()"), | |
| 112 | + QString("FBX file %1 could not be found.").arg(fbxInfo.fileName()), | |
| 113 | + QMessageBox::Ok); | |
| 114 | + qDebug() << "[Warning] " << QString("FBX file %1 could not be found.").arg(fbxInfo.fileName()); | |
| 115 | + return false; | |
| 116 | + } | |
| 117 | + // Compress and copy | |
| 118 | + if (!addPart(fbxInfo.filePath(), "fbx")) { | |
| 119 | + return false; | |
| 120 | + } | |
| 121 | + } else if (line[0] == TEXDIR_FIELD) { // Check existence | |
| 122 | + texDir = QFileInfo(fst).path() + "/" + line[1]; | |
| 123 | + QFileInfo texInfo(texDir); | |
| 124 | + if (!texInfo.exists() || !texInfo.isDir()) { | |
| 125 | + QMessageBox::warning(NULL, | |
| 126 | + QString("ModelUploader::zip()"), | |
| 127 | + QString("Texture directory could not be found."), | |
| 128 | + QMessageBox::Ok); | |
| 129 | + qDebug() << "[Warning] " << QString("Texture directory could not be found."); | |
| 130 | + return false; | |
| 131 | + } | |
| 132 | + } else if (line[0] == LOD_FIELD) { | |
| 133 | + QFileInfo lod(QFileInfo(fst).path() + "/" + line[1]); | |
| 134 | + if (!lod.exists() || !lod.isFile()) { // Check existence | |
| 135 | + QMessageBox::warning(NULL, | |
| 136 | + QString("ModelUploader::zip()"), | |
| 137 | + QString("LOD file %1 could not be found.").arg(lod.fileName()), | |
| 138 | + QMessageBox::Ok); | |
| 139 | + qDebug() << "[Warning] " << QString("FBX file %1 could not be found.").arg(lod.fileName()); | |
| 140 | + } | |
| 141 | + // Compress and copy | |
| 142 | + if (!addPart(lod.filePath(), QString("lod%1").arg(++_lodCount))) { | |
| 143 | + return false; | |
| 144 | + } | |
| 145 | + } | |
| 146 | + } | |
| 147 | + | |
| 148 | + if (!addTextures(texDir, fbxFile)) { | |
| 149 | + return false; | |
| 150 | + } | |
| 151 | + | |
| 152 | + QHttpPart textPart; | |
| 153 | + textPart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data;" | |
| 154 | + " name=\"model_category\""); | |
| 155 | + if (_isHead) { | |
| 156 | + textPart.setBody("heads"); | |
| 157 | + } else { | |
| 158 | + textPart.setBody("skeletons"); | |
| 159 | + } | |
| 160 | + _dataMultiPart->append(textPart); | |
| 161 | + | |
| 162 | + _readyToSend = true; | |
| 163 | + return true; | |
| 164 | +} | |
| 165 | + | |
| 166 | +void ModelUploader::send() { | |
| 167 | + if (!zip()) { | |
| 168 | + deleteLater(); | |
| 169 | + return; | |
| 170 | + } | |
| 171 | + | |
| 172 | + JSONCallbackParameters callbackParams; | |
| 173 | + callbackParams.jsonCallbackReceiver = this; | |
| 174 | + callbackParams.jsonCallbackMethod = "uploadSuccess"; | |
| 175 | + callbackParams.errorCallbackReceiver = this; | |
| 176 | + callbackParams.errorCallbackMethod = "uploadFailed"; | |
| 177 | + callbackParams.updateReciever = this; | |
| 178 | + callbackParams.updateSlot = SLOT(uploadUpdate(qint64, qint64)); | |
| 179 | + | |
| 180 | + AccountManager::getInstance().authenticatedRequest(MODEL_URL, QNetworkAccessManager::PostOperation, callbackParams, QByteArray(), _dataMultiPart); | |
| 181 | + _dataMultiPart = NULL; | |
| 182 | + qDebug() << "Sending model..."; | |
| 183 | + _progressDialog = new QDialog(); | |
| 184 | + _progressBar = new QProgressBar(_progressDialog); | |
| 185 | + _progressBar->setRange(0, 100); | |
| 186 | + _progressBar->setValue(0); | |
| 187 | + | |
| 188 | + _progressDialog->setWindowTitle("Uploading model..."); | |
| 189 | + _progressDialog->setLayout(new QGridLayout(_progressDialog)); | |
| 190 | + _progressDialog->layout()->addWidget(_progressBar); | |
| 191 | + | |
| 192 | + _progressDialog->exec(); | |
| 193 | + | |
| 194 | + delete _progressDialog; | |
| 195 | + _progressDialog = NULL; | |
| 196 | + _progressBar = NULL; | |
| 197 | +} | |
| 198 | + | |
| 199 | +void ModelUploader::uploadUpdate(qint64 bytesSent, qint64 bytesTotal) { | |
| 200 | + if (_progressDialog) { | |
| 201 | + _progressBar->setRange(0, bytesTotal); | |
| 202 | + _progressBar->setValue(bytesSent); | |
| 203 | + } | |
| 204 | +} | |
| 205 | + | |
| 206 | +void ModelUploader::uploadSuccess(const QJsonObject& jsonResponse) { | |
| 207 | + if (_progressDialog) { | |
| 208 | + _progressDialog->accept(); | |
| 209 | + } | |
| 210 | + QMessageBox::information(NULL, | |
| 211 | + QString("ModelUploader::uploadSuccess()"), | |
| 212 | + QString("We are reading your model information."), | |
| 213 | + QMessageBox::Ok); | |
| 214 | + qDebug() << "Model sent with success"; | |
| 215 | + checkS3(); | |
| 216 | +} | |
| 217 | + | |
| 218 | +void ModelUploader::uploadFailed(QNetworkReply::NetworkError errorCode, const QString& errorString) { | |
| 219 | + if (_progressDialog) { | |
| 220 | + _progressDialog->reject(); | |
| 221 | + } | |
| 222 | + QMessageBox::warning(NULL, | |
| 223 | + QString("ModelUploader::uploadFailed()"), | |
| 224 | + QString("There was a problem with your upload, please try again later."), | |
| 225 | + QMessageBox::Ok); | |
| 226 | + qDebug() << "Model upload failed (" << errorCode << "): " << errorString; | |
| 227 | + deleteLater(); | |
| 228 | +} | |
| 229 | + | |
| 230 | +void ModelUploader::checkS3() { | |
| 231 | + qDebug() << "Checking S3 for " << _url; | |
| 232 | + QNetworkRequest request(_url); | |
| 233 | + QNetworkReply* reply = _networkAccessManager.head(request); | |
| 234 | + connect(reply, SIGNAL(finished()), SLOT(processCheck())); | |
| 235 | +} | |
| 236 | + | |
| 237 | +void ModelUploader::processCheck() { | |
| 238 | + QNetworkReply* reply = static_cast<QNetworkReply*>(sender()); | |
| 239 | + _timer.stop(); | |
| 240 | + | |
| 241 | + switch (reply->error()) { | |
| 242 | + case QNetworkReply::NoError: | |
| 243 | + QMessageBox::information(NULL, | |
| 244 | + QString("ModelUploader::processCheck()"), | |
| 245 | + QString("Your model is now available in the browser."), | |
| 246 | + QMessageBox::Ok); | |
| 247 | + deleteLater(); | |
| 248 | + break; | |
| 249 | + case QNetworkReply::ContentNotFoundError: | |
| 250 | + if (--_numberOfChecks) { | |
| 251 | + _timer.start(TIMEOUT); | |
| 252 | + break; | |
| 253 | + } | |
| 254 | + default: | |
| 255 | + QMessageBox::warning(NULL, | |
| 256 | + QString("ModelUploader::processCheck()"), | |
| 257 | + QString("We could not verify that your model was sent sucessfully\n" | |
| 258 | + "but it may have. If you do not see it in the model browser, try to upload again."), | |
| 259 | + QMessageBox::Ok); | |
| 260 | + deleteLater(); | |
| 261 | + break; | |
| 262 | + } | |
| 263 | + | |
| 264 | + delete reply; | |
| 265 | +} | |
| 266 | + | |
| 267 | +bool ModelUploader::addTextures(const QString& texdir, const QString fbxFile) { | |
| 268 | + QFile fbx(fbxFile); | |
| 269 | + if (!fbx.open(QIODevice::ReadOnly)) { | |
| 270 | + return false; | |
| 271 | + } | |
| 272 | + | |
| 273 | + QByteArray buffer = fbx.readAll(); | |
| 274 | + QVariantHash variantHash = readMapping(buffer); | |
| 275 | + FBXGeometry geometry = readFBX(buffer, variantHash); | |
| 276 | + | |
| 277 | + foreach (FBXMesh mesh, geometry.meshes) { | |
| 278 | + foreach (FBXMeshPart part, mesh.parts) { | |
| 279 | + if (!part.diffuseFilename.isEmpty()) { | |
| 280 | + if (!addPart(QFileInfo(fbxFile).path() + "/" + part.diffuseFilename, | |
| 281 | + QString("texture%1").arg(++_texturesCount))) { | |
| 282 | + return false; | |
| 283 | + } | |
| 284 | + } | |
| 285 | + if (!part.normalFilename.isEmpty()) { | |
| 286 | + if (!addPart(QFileInfo(fbxFile).path() + "/" + part.normalFilename, | |
| 287 | + QString("texture%1").arg(++_texturesCount))) { | |
| 288 | + return false; | |
| 289 | + } | |
| 290 | + } | |
| 291 | + } | |
| 292 | + } | |
| 293 | + | |
| 294 | + return true; | |
| 295 | +} | |
| 296 | + | |
| 297 | +bool ModelUploader::addPart(const QString &path, const QString& name) { | |
| 298 | + qDebug() << path; | |
| 299 | + QFile file(path); | |
| 300 | + if (!file.open(QIODevice::ReadOnly)) { | |
| 301 | + QMessageBox::warning(NULL, | |
| 302 | + QString("ModelUploader::addPart()"), | |
| 303 | + QString("Could not open %1").arg(path), | |
| 304 | + QMessageBox::Ok); | |
| 305 | + qDebug() << "[Warning] " << QString("Could not open %1").arg(path); | |
| 306 | + return false; | |
| 307 | + } | |
| 308 | + QByteArray buffer = qCompress(file.readAll()); | |
| 309 | + | |
| 310 | + // Qt's qCompress() default compression level (-1) is the standard zLib compression. | |
| 311 | + // Here remove Qt's custom header that prevent the data server from uncompressing the files with zLib. | |
| 312 | + buffer.remove(QCOMPRESS_HEADER_POSITION, QCOMPRESS_HEADER_SIZE); | |
| 313 | + | |
| 314 | + QHttpPart part; | |
| 315 | + part.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data;" | |
| 316 | + " name=\"" + name.toUtf8() + "\";" | |
| 317 | + " filename=\"" + QFileInfo(file).fileName().toUtf8() + "\"")); | |
| 318 | + part.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("application/octet-stream")); | |
| 319 | + part.setBody(buffer); | |
| 320 | + _dataMultiPart->append(part); | |
| 321 | + | |
| 322 | + | |
| 323 | + qDebug() << "File " << QFileInfo(file).fileName() << " added to model."; | |
| 324 | + _totalSize += file.size(); | |
| 325 | + if (_totalSize > MAX_SIZE) { | |
| 326 | + QMessageBox::warning(NULL, | |
| 327 | + QString("ModelUploader::zip()"), | |
| 328 | + QString("Model too big, over %1 Bytes.").arg(MAX_SIZE), | |
| 329 | + QMessageBox::Ok); | |
| 330 | + qDebug() << "[Warning] " << QString("Model too big, over %1 Bytes.").arg(MAX_SIZE); | |
| 331 | + return false; | |
| 332 | + } | |
| 333 | + qDebug() << "Current model size: " << _totalSize; | |
| 334 | + | |
| 335 | + return true; | |
| 336 | +} | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |