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