src/PartDownloader.cc

changeset 706
d79083b9f74d
parent 705
09150d027e8c
parent 655
b376645315ab
child 707
c89b58ba266b
equal deleted inserted replaced
705:09150d027e8c 706:d79083b9f74d
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 }

mercurial