| 1 /* |
|
| 2 * LDForge: LDraw parts authoring CAD |
|
| 3 * Copyright (C) 2013, 2014 Santeri Piippo |
|
| 4 * |
|
| 5 * This program is free software: you can redistribute it and/or modify |
|
| 6 * it under the terms of the GNU General Public License as published by |
|
| 7 * the Free Software Foundation, either version 3 of the License, or |
|
| 8 * (at your option) any later version. |
|
| 9 * |
|
| 10 * This program is distributed in the hope that it will be useful, |
|
| 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
| 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
| 13 * GNU General Public License for more details. |
|
| 14 * |
|
| 15 * You should have received a copy of the GNU General Public License |
|
| 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
| 17 */ |
|
| 18 |
|
| 19 #include <QNetworkAccessManager> |
|
| 20 #include <QNetworkRequest> |
|
| 21 #include <QNetworkReply> |
|
| 22 #include <QDir> |
|
| 23 #include <QProgressBar> |
|
| 24 #include <QPushButton> |
|
| 25 #include "PartDownloader.h" |
|
| 26 #include "ui_downloadfrom.h" |
|
| 27 #include "Types.h" |
|
| 28 #include "MainWindow.h" |
|
| 29 #include "Document.h" |
|
| 30 #include "GLRenderer.h" |
|
| 31 #include "ConfigurationDialog.h" |
|
| 32 |
|
| 33 cfg (String, net_downloadpath, ""); |
|
| 34 cfg (Bool, net_guesspaths, true); |
|
| 35 cfg (Bool, net_autoclose, true); |
|
| 36 |
|
| 37 const QString g_unofficialLibraryURL ("http://ldraw.org/library/unofficial/"); |
|
| 38 |
|
| 39 // ============================================================================= |
|
| 40 // |
|
| 41 void PartDownloader::staticBegin() |
|
| 42 { |
|
| 43 QString path = getDownloadPath(); |
|
| 44 |
|
| 45 if (path == "" || QDir (path).exists() == false) |
|
| 46 { |
|
| 47 critical (PartDownloader::tr ("You need to specify a valid path for " |
|
| 48 "downloaded files in the configuration to download paths.")); |
|
| 49 |
|
| 50 (new ConfigDialog (ConfigDialog::DownloadTab, null))->exec(); |
|
| 51 return; |
|
| 52 } |
|
| 53 |
|
| 54 PartDownloader* dlg = new PartDownloader; |
|
| 55 dlg->exec(); |
|
| 56 } |
|
| 57 |
|
| 58 // ============================================================================= |
|
| 59 // |
|
| 60 QString PartDownloader::getDownloadPath() |
|
| 61 { |
|
| 62 QString path = net_downloadpath; |
|
| 63 |
|
| 64 #if DIRSLASH_CHAR != '/' |
|
| 65 path.replace (DIRSLASH, "/"); |
|
| 66 #endif |
|
| 67 |
|
| 68 return path; |
|
| 69 } |
|
| 70 |
|
| 71 // ============================================================================= |
|
| 72 // |
|
| 73 PartDownloader::PartDownloader (QWidget* parent) : QDialog (parent) |
|
| 74 { |
|
| 75 setInterface (new Ui_DownloadFrom); |
|
| 76 interface()->setupUi (this); |
|
| 77 interface()->fname->setFocus(); |
|
| 78 interface()->progress->horizontalHeader()->setResizeMode (PartLabelColumn, QHeaderView::Stretch); |
|
| 79 |
|
| 80 setDownloadButton (new QPushButton (tr ("Download"))); |
|
| 81 interface()->buttonBox->addButton (downloadButton(), QDialogButtonBox::ActionRole); |
|
| 82 getButton (Abort)->setEnabled (false); |
|
| 83 |
|
| 84 connect (interface()->source, SIGNAL (currentIndexChanged (int)), |
|
| 85 this, SLOT (sourceChanged (int))); |
|
| 86 connect (interface()->buttonBox, SIGNAL (clicked (QAbstractButton*)), |
|
| 87 this, SLOT (buttonClicked (QAbstractButton*))); |
|
| 88 } |
|
| 89 |
|
| 90 // ============================================================================= |
|
| 91 // |
|
| 92 PartDownloader::~PartDownloader() |
|
| 93 { |
|
| 94 delete interface(); |
|
| 95 } |
|
| 96 |
|
| 97 // ============================================================================= |
|
| 98 // |
|
| 99 QString PartDownloader::getURL() const |
|
| 100 { |
|
| 101 const Source src = getSource(); |
|
| 102 QString dest; |
|
| 103 |
|
| 104 switch (src) |
|
| 105 { |
|
| 106 case PartsTracker: |
|
| 107 dest = interface()->fname->text(); |
|
| 108 modifyDestination (dest); |
|
| 109 return g_unofficialLibraryURL + dest; |
|
| 110 |
|
| 111 case CustomURL: |
|
| 112 return interface()->fname->text(); |
|
| 113 } |
|
| 114 |
|
| 115 // Shouldn't happen |
|
| 116 return ""; |
|
| 117 } |
|
| 118 |
|
| 119 // ============================================================================= |
|
| 120 // |
|
| 121 void PartDownloader::modifyDestination (QString& dest) const |
|
| 122 { |
|
| 123 dest = dest.simplified(); |
|
| 124 |
|
| 125 // If the user doesn't want us to guess, stop right here. |
|
| 126 if (net_guesspaths == false) |
|
| 127 return; |
|
| 128 |
|
| 129 // Ensure .dat extension |
|
| 130 if (dest.right (4) != ".dat") |
|
| 131 { |
|
| 132 // Remove the existing extension, if any. It may be we're here over a |
|
| 133 // typo in the .dat extension. |
|
| 134 const int dotpos = dest.lastIndexOf ("."); |
|
| 135 |
|
| 136 if (dotpos != -1 && dotpos >= dest.length() - 4) |
|
| 137 dest.chop (dest.length() - dotpos); |
|
| 138 |
|
| 139 dest += ".dat"; |
|
| 140 } |
|
| 141 |
|
| 142 // If the part starts with s\ or s/, then use parts/s/. Same goes with |
|
| 143 // 48\ and p/48/. |
|
| 144 if (dest.left (2) == "s\\" || dest.left (2) == "s/") |
|
| 145 { |
|
| 146 dest.remove (0, 2); |
|
| 147 dest.prepend ("parts/s/"); |
|
| 148 } elif (dest.left (3) == "48\\" || dest.left (3) == "48/") |
|
| 149 { |
|
| 150 dest.remove (0, 3); |
|
| 151 dest.prepend ("p/48/"); |
|
| 152 } |
|
| 153 |
|
| 154 /* Try determine where to put this part. We have four directories: |
|
| 155 parts/, parts/s/, p/, and p/48/. If we haven't already specified |
|
| 156 either parts/ or p/, we need to add it automatically. Part files |
|
| 157 are numbers wit a possible u prefix for parts with unknown number |
|
| 158 which can be followed by any of: |
|
| 159 - c** (composites) |
|
| 160 - d** (formed stickers) |
|
| 161 - p** (patterns) |
|
| 162 - a lowercase alphabetic letter for variants |
|
| 163 |
|
| 164 Subfiles (usually) have an s** prefix, in which case we use parts/s/. |
|
| 165 Note that the regex starts with a '^' so it won't catch already fully |
|
| 166 given part file names. */ |
|
| 167 QString partRegex = "^u?[0-9]+(c[0-9][0-9]+)*(d[0-9][0-9]+)*[a-z]?(p[0-9a-z][0-9a-z]+)*"; |
|
| 168 QString subpartRegex = partRegex + "s[0-9][0-9]+"; |
|
| 169 |
|
| 170 partRegex += "\\.dat$"; |
|
| 171 subpartRegex += "\\.dat$"; |
|
| 172 |
|
| 173 if (QRegExp (subpartRegex).exactMatch (dest)) |
|
| 174 dest.prepend ("parts/s/"); |
|
| 175 elif (QRegExp (partRegex).exactMatch (dest)) |
|
| 176 dest.prepend ("parts/"); |
|
| 177 elif (dest.left (6) != "parts/" && dest.left (2) != "p/") |
|
| 178 dest.prepend ("p/"); |
|
| 179 } |
|
| 180 |
|
| 181 // ============================================================================= |
|
| 182 // |
|
| 183 PartDownloader::Source PartDownloader::getSource() const |
|
| 184 { |
|
| 185 return (Source) interface()->source->currentIndex(); |
|
| 186 } |
|
| 187 |
|
| 188 // ============================================================================= |
|
| 189 // |
|
| 190 void PartDownloader::sourceChanged (int i) |
|
| 191 { |
|
| 192 if (i == CustomURL) |
|
| 193 interface()->fileNameLabel->setText (tr ("URL:")); |
|
| 194 else |
|
| 195 interface()->fileNameLabel->setText (tr ("File name:")); |
|
| 196 } |
|
| 197 |
|
| 198 // ============================================================================= |
|
| 199 // |
|
| 200 void PartDownloader::buttonClicked (QAbstractButton* btn) |
|
| 201 { |
|
| 202 if (btn == getButton (Close)) |
|
| 203 { |
|
| 204 reject(); |
|
| 205 } |
|
| 206 elif (btn == getButton (Abort)) |
|
| 207 { |
|
| 208 setAborted (true); |
|
| 209 |
|
| 210 for (PartDownloadRequest* req : requests()) |
|
| 211 req->abort(); |
|
| 212 } |
|
| 213 elif (btn == getButton (Download)) |
|
| 214 { |
|
| 215 QString dest = interface()->fname->text(); |
|
| 216 setPrimaryFile (null); |
|
| 217 setAborted (false); |
|
| 218 |
|
| 219 if (getSource() == CustomURL) |
|
| 220 dest = basename (getURL()); |
|
| 221 |
|
| 222 modifyDestination (dest); |
|
| 223 |
|
| 224 if (QFile::exists (PartDownloader::getDownloadPath() + DIRSLASH + dest)) |
|
| 225 { |
|
| 226 const QString overwritemsg = format (tr ("%1 already exists in download directory. Overwrite?"), dest); |
|
| 227 if (!confirm (tr ("Overwrite?"), overwritemsg)) |
|
| 228 return; |
|
| 229 } |
|
| 230 |
|
| 231 downloadButton()->setEnabled (false); |
|
| 232 interface()->progress->setEnabled (true); |
|
| 233 interface()->fname->setEnabled (false); |
|
| 234 interface()->source->setEnabled (false); |
|
| 235 downloadFile (dest, getURL(), true); |
|
| 236 getButton (Close)->setEnabled (false); |
|
| 237 getButton (Abort)->setEnabled (true); |
|
| 238 getButton (Download)->setEnabled (false); |
|
| 239 } |
|
| 240 } |
|
| 241 |
|
| 242 // ============================================================================= |
|
| 243 // |
|
| 244 void PartDownloader::downloadFile (QString dest, QString url, bool primary) |
|
| 245 { |
|
| 246 const int row = interface()->progress->rowCount(); |
|
| 247 |
|
| 248 // Don't download files repeadetly. |
|
| 249 if (filesToDownload().indexOf (dest) != -1) |
|
| 250 return; |
|
| 251 |
|
| 252 modifyDestination (dest); |
|
| 253 PartDownloadRequest* req = new PartDownloadRequest (url, dest, primary, this); |
|
| 254 m_filesToDownload << dest; |
|
| 255 m_requests << req; |
|
| 256 interface()->progress->insertRow (row); |
|
| 257 req->setTableRow (row); |
|
| 258 req->updateToTable(); |
|
| 259 } |
|
| 260 |
|
| 261 // ============================================================================= |
|
| 262 // |
|
| 263 void PartDownloader::checkIfFinished() |
|
| 264 { |
|
| 265 bool failed = isAborted(); |
|
| 266 |
|
| 267 // If there is some download still working, we're not finished. |
|
| 268 for (PartDownloadRequest* req : requests()) |
|
| 269 { |
|
| 270 if (!req->isFinished()) |
|
| 271 return; |
|
| 272 |
|
| 273 if (req->state() == PartDownloadRequest::EFailed) |
|
| 274 failed = true; |
|
| 275 } |
|
| 276 |
|
| 277 for (PartDownloadRequest* req : requests()) |
|
| 278 delete req; |
|
| 279 |
|
| 280 m_requests.clear(); |
|
| 281 |
|
| 282 // Update everything now |
|
| 283 if (primaryFile() != null) |
|
| 284 { |
|
| 285 LDDocument::setCurrent (primaryFile()); |
|
| 286 reloadAllSubfiles(); |
|
| 287 g_win->doFullRefresh(); |
|
| 288 g_win->R()->resetAngles(); |
|
| 289 } |
|
| 290 |
|
| 291 if (net_autoclose && !failed) |
|
| 292 { |
|
| 293 // Close automatically if desired. |
|
| 294 accept(); |
|
| 295 } |
|
| 296 else |
|
| 297 { |
|
| 298 // Allow the prompt be closed now. |
|
| 299 getButton (Abort)->setEnabled (false); |
|
| 300 getButton (Close)->setEnabled (true); |
|
| 301 } |
|
| 302 } |
|
| 303 |
|
| 304 // ============================================================================= |
|
| 305 // |
|
| 306 QPushButton* PartDownloader::getButton (PartDownloader::Button i) |
|
| 307 { |
|
| 308 switch (i) |
|
| 309 { |
|
| 310 case Download: |
|
| 311 return downloadButton(); |
|
| 312 |
|
| 313 case Abort: |
|
| 314 return qobject_cast<QPushButton*> (interface()->buttonBox->button (QDialogButtonBox::Abort)); |
|
| 315 |
|
| 316 case Close: |
|
| 317 return qobject_cast<QPushButton*> (interface()->buttonBox->button (QDialogButtonBox::Close)); |
|
| 318 } |
|
| 319 |
|
| 320 return null; |
|
| 321 } |
|
| 322 |
|
| 323 // ============================================================================= |
|
| 324 // |
|
| 325 PartDownloadRequest::PartDownloadRequest (QString url, QString dest, bool primary, PartDownloader* parent) : |
|
| 326 QObject (parent), |
|
| 327 m_state (ERequesting), |
|
| 328 m_prompt (parent), |
|
| 329 m_url (url), |
|
| 330 m_destinaton (dest), |
|
| 331 m_filePath (PartDownloader::getDownloadPath() + DIRSLASH + dest), |
|
| 332 m_networkManager (new QNetworkAccessManager), |
|
| 333 m_isFirstUpdate (true), |
|
| 334 m_isPrimary (primary), |
|
| 335 m_filePointer (null) |
|
| 336 { |
|
| 337 // Make sure that we have a valid destination. |
|
| 338 QString dirpath = dirname (filePath()); |
|
| 339 |
|
| 340 QDir dir (dirpath); |
|
| 341 |
|
| 342 if (dir.exists() == false) |
|
| 343 { |
|
| 344 print ("Creating %1...\n", dirpath); |
|
| 345 |
|
| 346 if (!dir.mkpath (dirpath)) |
|
| 347 critical (format (tr ("Couldn't create the directory %1!"), dirpath)); |
|
| 348 } |
|
| 349 |
|
| 350 setNetworkReply (networkManager()->get (QNetworkRequest (QUrl (url)))); |
|
| 351 connect (networkReply(), SIGNAL (finished()), this, SLOT (downloadFinished())); |
|
| 352 connect (networkReply(), SIGNAL (readyRead()), this, SLOT (readyRead())); |
|
| 353 connect (networkReply(), SIGNAL (downloadProgress (qint64, qint64)), |
|
| 354 this, SLOT (downloadProgress (qint64, qint64))); |
|
| 355 } |
|
| 356 |
|
| 357 // ============================================================================= |
|
| 358 // |
|
| 359 PartDownloadRequest::~PartDownloadRequest() {} |
|
| 360 |
|
| 361 // ============================================================================= |
|
| 362 // |
|
| 363 void PartDownloadRequest::updateToTable() |
|
| 364 { |
|
| 365 const int labelcol = PartDownloader::PartLabelColumn, |
|
| 366 progcol = PartDownloader::ProgressColumn; |
|
| 367 QTableWidget* table = prompt()->interface()->progress; |
|
| 368 QProgressBar* prog; |
|
| 369 |
|
| 370 switch (state()) |
|
| 371 { |
|
| 372 case ERequesting: |
|
| 373 case EDownloading: |
|
| 374 { |
|
| 375 prog = qobject_cast<QProgressBar*> (table->cellWidget (tableRow(), progcol)); |
|
| 376 |
|
| 377 if (!prog) |
|
| 378 { |
|
| 379 prog = new QProgressBar; |
|
| 380 table->setCellWidget (tableRow(), progcol, prog); |
|
| 381 } |
|
| 382 |
|
| 383 prog->setRange (0, numBytesTotal()); |
|
| 384 prog->setValue (numBytesRead()); |
|
| 385 } break; |
|
| 386 |
|
| 387 case EFinished: |
|
| 388 case EFailed: |
|
| 389 { |
|
| 390 const QString text = (state() == EFinished) |
|
| 391 ? "<b><span style=\"color: #080\">FINISHED</span></b>" |
|
| 392 : "<b><span style=\"color: #800\">FAILED</span></b>"; |
|
| 393 |
|
| 394 QLabel* lb = new QLabel (text); |
|
| 395 lb->setAlignment (Qt::AlignCenter); |
|
| 396 table->setCellWidget (tableRow(), progcol, lb); |
|
| 397 } break; |
|
| 398 } |
|
| 399 |
|
| 400 QLabel* lb = qobject_cast<QLabel*> (table->cellWidget (tableRow(), labelcol)); |
|
| 401 |
|
| 402 if (isFirstUpdate()) |
|
| 403 { |
|
| 404 lb = new QLabel (format ("<b>%1</b>", destinaton()), table); |
|
| 405 table->setCellWidget (tableRow(), labelcol, lb); |
|
| 406 } |
|
| 407 |
|
| 408 // Make sure that the cell is big enough to contain the label |
|
| 409 if (table->columnWidth (labelcol) < lb->width()) |
|
| 410 table->setColumnWidth (labelcol, lb->width()); |
|
| 411 |
|
| 412 setFirstUpdate (true); |
|
| 413 } |
|
| 414 |
|
| 415 // ============================================================================= |
|
| 416 // |
|
| 417 void PartDownloadRequest::downloadFinished() |
|
| 418 { |
|
| 419 if (networkReply()->error() != QNetworkReply::NoError) |
|
| 420 { |
|
| 421 if (isPrimary() && !prompt()->isAborted()) |
|
| 422 critical (networkReply()->errorString()); |
|
| 423 |
|
| 424 setState (EFailed); |
|
| 425 } |
|
| 426 elif (state() != EFailed) |
|
| 427 setState (EFinished); |
|
| 428 |
|
| 429 setNumBytesRead (numBytesTotal()); |
|
| 430 updateToTable(); |
|
| 431 |
|
| 432 if (filePointer()) |
|
| 433 { |
|
| 434 filePointer()->close(); |
|
| 435 delete filePointer(); |
|
| 436 setFilePointer (null); |
|
| 437 |
|
| 438 if (state() == EFailed) |
|
| 439 QFile::remove (filePath()); |
|
| 440 } |
|
| 441 |
|
| 442 if (state() != EFinished) |
|
| 443 { |
|
| 444 prompt()->checkIfFinished(); |
|
| 445 return; |
|
| 446 } |
|
| 447 |
|
| 448 // Try to load this file now. |
|
| 449 LDDocument* f = openDocument (filePath(), false); |
|
| 450 |
|
| 451 if (!f) |
|
| 452 return; |
|
| 453 |
|
| 454 f->setImplicit (!isPrimary()); |
|
| 455 |
|
| 456 // Iterate through this file and check for errors. If there's any that stems |
|
| 457 // from unknown file references, try resolve that by downloading the reference. |
|
| 458 // This is why downloading a part may end up downloading multiple files, as |
|
| 459 // it resolves dependencies. |
|
| 460 for (LDObject* obj : f->objects()) |
|
| 461 { |
|
| 462 LDError* err = dynamic_cast<LDError*> (obj); |
|
| 463 |
|
| 464 if (err == null || err->fileReferenced().isEmpty()) |
|
| 465 continue; |
|
| 466 |
|
| 467 QString dest = err->fileReferenced(); |
|
| 468 prompt()->modifyDestination (dest); |
|
| 469 prompt()->downloadFile (dest, g_unofficialLibraryURL + dest, false); |
|
| 470 } |
|
| 471 |
|
| 472 if (isPrimary()) |
|
| 473 { |
|
| 474 addRecentFile (filePath()); |
|
| 475 prompt()->setPrimaryFile (f); |
|
| 476 } |
|
| 477 |
|
| 478 prompt()->checkIfFinished(); |
|
| 479 } |
|
| 480 |
|
| 481 // ============================================================================= |
|
| 482 // |
|
| 483 void PartDownloadRequest::downloadProgress (int64 recv, int64 total) |
|
| 484 { |
|
| 485 setNumBytesRead (recv); |
|
| 486 setNumBytesTotal (total); |
|
| 487 setState (EDownloading); |
|
| 488 updateToTable(); |
|
| 489 } |
|
| 490 |
|
| 491 // ============================================================================= |
|
| 492 // |
|
| 493 void PartDownloadRequest::readyRead() |
|
| 494 { |
|
| 495 if (state() == EFailed) |
|
| 496 return; |
|
| 497 |
|
| 498 if (filePointer() == null) |
|
| 499 { |
|
| 500 m_filePath.replace ("\\", "/"); |
|
| 501 |
|
| 502 // We have already asked the user whether we can overwrite so we're good |
|
| 503 // to go here. |
|
| 504 setFilePointer (new QFile (filePath().toLocal8Bit())); |
|
| 505 |
|
| 506 if (!filePointer()->open (QIODevice::WriteOnly)) |
|
| 507 { |
|
| 508 critical (format (tr ("Couldn't open %1 for writing: %2"), filePath(), strerror (errno))); |
|
| 509 setState (EFailed); |
|
| 510 networkReply()->abort(); |
|
| 511 updateToTable(); |
|
| 512 prompt()->checkIfFinished(); |
|
| 513 return; |
|
| 514 } |
|
| 515 } |
|
| 516 |
|
| 517 filePointer()->write (networkReply()->readAll()); |
|
| 518 } |
|
| 519 |
|
| 520 // ============================================================================= |
|
| 521 // |
|
| 522 bool PartDownloadRequest::isFinished() const |
|
| 523 { |
|
| 524 return state() == EFinished || state() == EFailed; |
|
| 525 } |
|
| 526 |
|
| 527 // ============================================================================= |
|
| 528 // |
|
| 529 void PartDownloadRequest::abort() |
|
| 530 { |
|
| 531 networkReply()->abort(); |
|
| 532 } |
|
| 533 |
|
| 534 // ============================================================================= |
|
| 535 // |
|
| 536 DEFINE_ACTION (DownloadFrom, 0) |
|
| 537 { |
|
| 538 PartDownloader::staticBegin(); |
|
| 539 } |
|