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