--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/download.cc Fri Dec 13 20:01:49 2013 +0200 @@ -0,0 +1,496 @@ +/* + * LDForge: LDraw parts authoring CAD + * Copyright (C) 2013 Santeri Piippo + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <QNetworkAccessManager> +#include <QNetworkRequest> +#include <QNetworkReply> +#include <QDir> +#include <QProgressBar> +#include <QPushButton> +#include "download.h" +#include "ui_downloadfrom.h" +#include "types.h" +#include "gui.h" +#include "document.h" +#include "gldraw.h" +#include "configDialog.h" +#include "moc_download.cpp" + +cfg (String, net_downloadpath, ""); +cfg (Bool, net_guesspaths, true); +cfg (Bool, net_autoclose, true); + +const str g_unofficialLibraryURL ("http://ldraw.org/library/unofficial/"); + +// ============================================================================= +// ----------------------------------------------------------------------------- +void PartDownloader::staticBegin() +{ str path = getDownloadPath(); + + if (path == "" || QDir (path).exists() == false) + { critical (PartDownloader::tr ("You need to specify a valid path for " + "downloaded files in the configuration to download paths.")); + + (new ConfigDialog (ConfigDialog::DownloadTab, null))->exec(); + return; + } + + PartDownloader* dlg = new PartDownloader; + dlg->exec(); +} + +// ============================================================================= +// ----------------------------------------------------------------------------- +str PartDownloader::getDownloadPath() +{ str path = net_downloadpath; + +#if DIRSLASH_CHAR != '/' + path.replace (DIRSLASH, "/"); +#endif + + return path; +} + +// ============================================================================= +// ----------------------------------------------------------------------------- +PartDownloader::PartDownloader (QWidget* parent) : QDialog (parent) +{ setInterface (new Ui_DownloadFrom); + getInterface()->setupUi (this); + getInterface()->fname->setFocus(); + getInterface()->progress->horizontalHeader()->setResizeMode (PartLabelColumn, QHeaderView::Stretch); + + setDownloadButton (new QPushButton (tr ("Download"))); + getInterface()->buttonBox->addButton (getDownloadButton(), QDialogButtonBox::ActionRole); + getButton (Abort)->setEnabled (false); + + connect (getInterface()->source, SIGNAL (currentIndexChanged (int)), + this, SLOT (sourceChanged (int))); + connect (getInterface()->buttonBox, SIGNAL (clicked (QAbstractButton*)), + this, SLOT (buttonClicked (QAbstractButton*))); +} + +// ============================================================================= +// ----------------------------------------------------------------------------- +PartDownloader::~PartDownloader() +{ delete getInterface(); +} + +// ============================================================================= +// ----------------------------------------------------------------------------- +str PartDownloader::getURL() const +{ const Source src = getSource(); + str dest; + + switch (src) + { case PartsTracker: + dest = getInterface()->fname->text(); + modifyDestination (dest); + return g_unofficialLibraryURL + dest; + + case CustomURL: + return getInterface()->fname->text(); + } + + // Shouldn't happen + return ""; +} + +// ============================================================================= +// ----------------------------------------------------------------------------- +void PartDownloader::modifyDestination (str& dest) const +{ dest = dest.simplified(); + + // If the user doesn't want us to guess, stop right here. + if (net_guesspaths == false) + return; + + // Ensure .dat extension + if (dest.right (4) != ".dat") + { // Remove the existing extension, if any. It may be we're here over a + // typo in the .dat extension. + const int dotpos = dest.lastIndexOf ("."); + + if (dotpos != -1 && dotpos >= dest.length() - 4) + dest.chop (dest.length() - dotpos); + + dest += ".dat"; + } + + // If the part starts with s\ or s/, then use parts/s/. Same goes with + // 48\ and p/48/. + if (dest.left (2) == "s\\" || dest.left (2) == "s/") + { dest.remove (0, 2); + dest.prepend ("parts/s/"); + } elif (dest.left (3) == "48\\" || dest.left (3) == "48/") + { dest.remove (0, 3); + dest.prepend ("p/48/"); + } + + /* Try determine where to put this part. We have four directories: + parts/, parts/s/, p/, and p/48/. If we haven't already specified + either parts/ or p/, we need to add it automatically. Part files + are numbers wit a possible u prefix for parts with unknown number + which can be followed by any of: + - c** (composites) + - d** (formed stickers) + - p** (patterns) + - a lowercase alphabetic letter for variants + + Subfiles (usually) have an s** prefix, in which case we use parts/s/. + Note that the regex starts with a '^' so it won't catch already fully + given part file names. */ + str partRegex = "^u?[0-9]+(c[0-9][0-9]+)*(d[0-9][0-9]+)*[a-z]?(p[0-9a-z][0-9a-z]+)*"; + str subpartRegex = partRegex + "s[0-9][0-9]+"; + + partRegex += "\\.dat$"; + subpartRegex += "\\.dat$"; + + if (QRegExp (subpartRegex).exactMatch (dest)) + dest.prepend ("parts/s/"); + elif (QRegExp (partRegex).exactMatch (dest)) + dest.prepend ("parts/"); + elif (dest.left (6) != "parts/" && dest.left (2) != "p/") + dest.prepend ("p/"); +} + +// ============================================================================= +// ----------------------------------------------------------------------------- +PartDownloader::Source PartDownloader::getSource() const +{ return (Source) getInterface()->source->currentIndex(); +} + +// ============================================================================= +// ----------------------------------------------------------------------------- +void PartDownloader::sourceChanged (int i) +{ if (i == CustomURL) + getInterface()->fileNameLabel->setText (tr ("URL:")); + else + getInterface()->fileNameLabel->setText (tr ("File name:")); +} + +// ============================================================================= +// ----------------------------------------------------------------------------- +void PartDownloader::buttonClicked (QAbstractButton* btn) +{ if (btn == getButton (Close)) + { reject(); + } + elif (btn == getButton (Abort)) + { setAborted (true); + + for (PartDownloadRequest* req : getRequests()) + req->abort(); + } + elif (btn == getButton (Download)) + { str dest = getInterface()->fname->text(); + setPrimaryFile (null); + setAborted (false); + + if (getSource() == CustomURL) + dest = basename (getURL()); + + modifyDestination (dest); + + if (QFile::exists (PartDownloader::getDownloadPath() + DIRSLASH + dest)) + { const str overwritemsg = fmt (tr ("%1 already exists in download directory. Overwrite?"), dest); + if (!confirm (tr ("Overwrite?"), overwritemsg)) + return; + } + + getDownloadButton()->setEnabled (false); + getInterface()->progress->setEnabled (true); + getInterface()->fname->setEnabled (false); + getInterface()->source->setEnabled (false); + downloadFile (dest, getURL(), true); + getButton (Close)->setEnabled (false); + getButton (Abort)->setEnabled (true); + getButton (Download)->setEnabled (false); + } +} + +// ============================================================================= +// ----------------------------------------------------------------------------- +void PartDownloader::downloadFile (str dest, str url, bool primary) +{ const int row = getInterface()->progress->rowCount(); + + // Don't download files repeadetly. + if (getFilesToDownload().indexOf (dest) != -1) + return; + + modifyDestination (dest); + log ("DOWNLOAD: %1 -> %2\n", url, PartDownloader::getDownloadPath() + DIRSLASH + dest); + PartDownloadRequest* req = new PartDownloadRequest (url, dest, primary, this); + + pushToFilesToDownload (dest); + pushToRequests (req); + getInterface()->progress->insertRow (row); + req->setTableRow (row); + req->updateToTable(); +} + +// ============================================================================= +// ----------------------------------------------------------------------------- +void PartDownloader::checkIfFinished() +{ bool failed = isAborted(); + + // If there is some download still working, we're not finished. + for (PartDownloadRequest* req : getRequests()) + { if (!req->isFinished()) + return; + + if (req->getState() == PartDownloadRequest::EFailed) + failed = true; + } + + for (PartDownloadRequest* req : getRequests()) + delete req; + + clearRequests(); + + // Update everything now + if (getPrimaryFile()) + { LDDocument::setCurrent (getPrimaryFile()); + reloadAllSubfiles(); + g_win->doFullRefresh(); + g_win->R()->resetAngles(); + } + + if (net_autoclose && !failed) + { // Close automatically if desired. + accept(); + } + else + { // Allow the prompt be closed now. + getButton (Abort)->setEnabled (false); + getButton (Close)->setEnabled (true); + } +} + +// ============================================================================= +// ----------------------------------------------------------------------------- +QPushButton* PartDownloader::getButton (PartDownloader::Button i) +{ switch (i) + { case Download: + return getDownloadButton(); + + case Abort: + return qobject_cast<QPushButton*> (getInterface()->buttonBox->button (QDialogButtonBox::Abort)); + + case Close: + return qobject_cast<QPushButton*> (getInterface()->buttonBox->button (QDialogButtonBox::Close)); + } + + return null; +} + +// ============================================================================= +// ----------------------------------------------------------------------------- +PartDownloadRequest::PartDownloadRequest (str url, str dest, bool primary, PartDownloader* parent) : + QObject (parent), + m_State (ERequesting), + m_Prompt (parent), + m_URL (url), + m_Destinaton (dest), + m_FilePath (PartDownloader::getDownloadPath() + DIRSLASH + dest), + m_NAM (new QNetworkAccessManager), + m_FirstUpdate (true), + m_Primary (primary), + m_FilePointer (null) +{ + // Make sure that we have a valid destination. + str dirpath = dirname (getFilePath()); + + QDir dir (dirpath); + + if (!dir.exists()) + { log ("Creating %1...\n", dirpath); + + if (!dir.mkpath (dirpath)) + critical (fmt (tr ("Couldn't create the directory %1!"), dirpath)); + } + + setReply (getNAM()->get (QNetworkRequest (QUrl (url)))); + connect (getReply(), SIGNAL (finished()), this, SLOT (downloadFinished())); + connect (getReply(), SIGNAL (readyRead()), this, SLOT (readyRead())); + connect (getReply(), SIGNAL (downloadProgress (qint64, qint64)), + this, SLOT (downloadProgress (qint64, qint64))); +} + +// ============================================================================= +// ----------------------------------------------------------------------------- +PartDownloadRequest::~PartDownloadRequest() {} + +// ============================================================================= +// ----------------------------------------------------------------------------- +void PartDownloadRequest::updateToTable() +{ const int labelcol = PartDownloader::PartLabelColumn, + progcol = PartDownloader::ProgressColumn; + QTableWidget* table = getPrompt()->getInterface()->progress; + QProgressBar* prog; + + switch (getState()) + { case ERequesting: + case EDownloading: + { prog = qobject_cast<QProgressBar*> (table->cellWidget (getTableRow(), progcol)); + + if (!prog) + { prog = new QProgressBar; + table->setCellWidget (getTableRow(), progcol, prog); + } + + prog->setRange (0, getBytesTotal()); + prog->setValue (getBytesRead()); + } break; + + case EFinished: + case EFailed: + { const str text = (getState() == EFinished) + ? "<b><span style=\"color: #080\">FINISHED</span></b>" + : "<b><span style=\"color: #800\">FAILED</span></b>"; + + QLabel* lb = new QLabel (text); + lb->setAlignment (Qt::AlignCenter); + table->setCellWidget (getTableRow(), progcol, lb); + } break; + } + + QLabel* lb = qobject_cast<QLabel*> (table->cellWidget (getTableRow(), labelcol)); + + if (isFirstUpdate()) + { lb = new QLabel (fmt ("<b>%1</b>", getDestinaton()), table); + table->setCellWidget (getTableRow(), labelcol, lb); + } + + // Make sure that the cell is big enough to contain the label + if (table->columnWidth (labelcol) < lb->width()) + table->setColumnWidth (labelcol, lb->width()); + + setFirstUpdate (true); +} + +// ============================================================================= +// ----------------------------------------------------------------------------- +void PartDownloadRequest::downloadFinished() +{ if (getReply()->error() != QNetworkReply::NoError) + { if (isPrimary() && !getPrompt()->isAborted()) + critical (getReply()->errorString()); + + setState (EFailed); + } + elif (getState() != EFailed) + setState (EFinished); + + setBytesRead (getBytesTotal()); + updateToTable(); + + if (getFilePointer()) + { getFilePointer()->close(); + delete getFilePointer(); + setFilePointer (null); + + if (getState() == EFailed) + QFile::remove (getFilePath()); + } + + if (getState() != EFinished) + { getPrompt()->checkIfFinished(); + return; + } + + // Try to load this file now. + LDDocument* f = openDocument (getFilePath(), false); + + if (!f) + return; + + f->setImplicit (!isPrimary()); + + // Iterate through this file and check for errors. If there's any that stems + // from unknown file references, try resolve that by downloading the reference. + // This is why downloading a part may end up downloading multiple files, as + // it resolves dependencies. + for (LDObject* obj : f->getObjects()) + { LDError* err = dynamic_cast<LDError*> (obj); + + if (!err || err->getFileReferenced().isEmpty()) + continue; + + str dest = err->getFileReferenced(); + getPrompt()->modifyDestination (dest); + getPrompt()->downloadFile (dest, g_unofficialLibraryURL + dest, false); + } + + if (isPrimary()) + { addRecentFile (getFilePath()); + getPrompt()->setPrimaryFile (f); + } + + getPrompt()->checkIfFinished(); +} + +// ============================================================================= +// ----------------------------------------------------------------------------- +void PartDownloadRequest::downloadProgress (int64 recv, int64 total) +{ setBytesRead (recv); + setBytesTotal (total); + setState (EDownloading); + updateToTable(); +} + +// ============================================================================= +// ----------------------------------------------------------------------------- +void PartDownloadRequest::readyRead() +{ if (getState() == EFailed) + return; + + if (getFilePointer() == null) + { replaceInFilePath ("\\", "/"); + + // We have already asked the user whether we can overwrite so we're good + // to go here. + setFilePointer (new QFile (getFilePath().toLocal8Bit())); + + if (!getFilePointer()->open (QIODevice::WriteOnly)) + { critical (fmt (tr ("Couldn't open %1 for writing: %2"), getFilePath(), strerror (errno))); + setState (EFailed); + getReply()->abort(); + updateToTable(); + getPrompt()->checkIfFinished(); + return; + } + } + + getFilePointer()->write (getReply()->readAll()); +} + +// ============================================================================= +// ----------------------------------------------------------------------------- +bool PartDownloadRequest::isFinished() const +{ return getState() == EFinished || getState() == EFailed; +} + +// ============================================================================= +// ----------------------------------------------------------------------------- +void PartDownloadRequest::abort() +{ getReply()->abort(); +} + +// ============================================================================= +// ----------------------------------------------------------------------------- +DEFINE_ACTION (DownloadFrom, 0) +{ PartDownloader::staticBegin(); +}