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