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 | |||