diff --git a/Changelog b/Changelog index de440a073..6544cad6d 100644 --- a/Changelog +++ b/Changelog @@ -1,6 +1,7 @@ * Unreleased - Christophe Dumez - v3.0.0 - FEATURE: Brand new torrent addition dialog - FEATURE: Add the ability to choose the save path when using magnet links (mutoso) + - FEATURE: Add support for adding multiple local torrents at once (Web UI) - COSMETIC: Improve style of left panel - BUGFIX: Lower panels no longer gets disabled - OTHER: Drop support for libtorrent v0.14.x diff --git a/src/webui/html/upload.html b/src/webui/html/upload.html index 178f27d56..2140f181f 100644 --- a/src/webui/html/upload.html +++ b/src/webui/html/upload.html @@ -6,19 +6,58 @@ +
-
-

_(Download local torrent)

-

- +
+
+ +
-
diff --git a/src/webui/httpconnection.cpp b/src/webui/httpconnection.cpp index 100066478..3829f6cc4 100644 --- a/src/webui/httpconnection.cpp +++ b/src/webui/httpconnection.cpp @@ -419,23 +419,26 @@ void HttpConnection::respondCommand(const QString& command) { } if (command == "upload") { qDebug() << Q_FUNC_INFO << "upload"; - // Get a unique filename - QTemporaryFile *tmpfile = new QTemporaryFile (QDir::temp().absoluteFilePath("qBT-XXXXXX.torrent")); - tmpfile->setAutoRemove(false); - if (tmpfile->open()) { - QString filePath = tmpfile->fileName(); - tmpfile->write(m_parser.torrent()); - tmpfile->close(); - // XXX: tmpfile needs to be deleted on Windows before using the file - // or it will complain that the file is used by another process. - delete tmpfile; - emit torrentReadyToBeDownloaded(filePath, false, QString(), false); - // Clean up - QFile::remove(filePath); - } else { - std::cerr << "I/O Error: Could not create temporary file" << std::endl; - delete tmpfile; - return; + const QList& torrents = m_parser.torrents(); + foreach(const QByteArray& torrentContent, torrents) { + // Get a unique filename + QTemporaryFile *tmpfile = new QTemporaryFile (QDir::temp().absoluteFilePath("qBT-XXXXXX.torrent")); + tmpfile->setAutoRemove(false); + if (tmpfile->open()) { + QString filePath = tmpfile->fileName(); + tmpfile->write(torrentContent); + tmpfile->close(); + // XXX: tmpfile needs to be deleted on Windows before using the file + // or it will complain that the file is used by another process. + delete tmpfile; + emit torrentReadyToBeDownloaded(filePath, false, QString(), false); + // Clean up + QFile::remove(filePath); + } else { + std::cerr << "I/O Error: Could not create temporary file" << std::endl; + delete tmpfile; + return; + } } // Prepare response m_generator.setStatusLine(200, "OK"); diff --git a/src/webui/httprequestparser.cpp b/src/webui/httprequestparser.cpp index 77881c46e..51c15b387 100644 --- a/src/webui/httprequestparser.cpp +++ b/src/webui/httprequestparser.cpp @@ -61,8 +61,8 @@ QString HttpRequestParser::post(const QString& key) const { return m_postMap.value(key); } -const QByteArray& HttpRequestParser::torrent() const { - return m_torrentContent; +const QList& HttpRequestParser::torrents() const { + return m_torrents; } void HttpRequestParser::writeHeader(const QByteArray& ba) { @@ -79,6 +79,18 @@ void HttpRequestParser::writeHeader(const QByteArray& ba) { } } +static QList splitRawData(QByteArray rawData, const QByteArray& sep) +{ + QList ret; + const int sepLength = sep.size(); + int index = 0; + while ((index = rawData.indexOf(sep)) >= 0) { + ret << rawData.left(index); + rawData = rawData.mid(index + sepLength); + } + return ret; +} + void HttpRequestParser::writeMessage(const QByteArray& ba) { // Parse message content Q_ASSERT (m_header.hasContentLength()); @@ -119,21 +131,29 @@ Submit Query **/ if (m_header.contentType().startsWith("multipart/form-data")) { qDebug() << Q_FUNC_INFO << "header is: " << m_header.toString(); - - int filename_index = m_data.indexOf("filename="); - if (filename_index >= 0) { - QByteArray boundary = m_data.left(m_data.indexOf("\r\n")); - qDebug() << "Boundary is " << boundary << "\n\n"; - qDebug() << "Before binary data: " << m_data.left(m_data.indexOf("\r\n\r\n", filename_index+9)) << "\n\n"; - m_torrentContent = m_data.mid(m_data.indexOf("\r\n\r\n", filename_index+9) + 4); - int binaryend_index = m_torrentContent.indexOf("\r\n"+boundary); - if (binaryend_index >= 0) { - qDebug() << "found end boundary :)"; - m_torrentContent = m_torrentContent.left(binaryend_index); + static QRegExp boundaryRegexQuoted("boundary=\"([ \\w'()+,-\\./:=\\?]+)\""); + static QRegExp boundaryRegexNotQuoted("boundary=([\\w'()+,-\\./:=\\?]+)"); + QByteArray boundary; + if (boundaryRegexQuoted.indexIn(m_header.toString()) < 0) { + if (boundaryRegexNotQuoted.indexIn(m_header.toString()) < 0) { + qWarning() << "Could not find boundary in multipart/form-data header!"; + m_error = true; + return; + } else { + boundary = "--" + boundaryRegexNotQuoted.cap(1).toAscii(); } - qDebug() << Q_FUNC_INFO << "m_torrentContent.size(): " << m_torrentContent.size()<< "\n\n"; } else { - m_error = true; + boundary = "--" + boundaryRegexQuoted.cap(1).toAscii(); + } + qDebug() << "Boundary is " << boundary; + QList parts = splitRawData(m_data, boundary); + qDebug() << parts.size() << "parts in data"; + foreach (const QByteArray& part, parts) { + const int filenameIndex = part.indexOf("filename="); + if (filenameIndex < 0) + continue; + qDebug() << "Found a torrent"; + m_torrents << part.mid(part.indexOf("\r\n\r\n", filenameIndex + 9) + 4); } } } diff --git a/src/webui/httprequestparser.h b/src/webui/httprequestparser.h index 999aeefa6..64837255b 100644 --- a/src/webui/httprequestparser.h +++ b/src/webui/httprequestparser.h @@ -45,7 +45,7 @@ public: const QByteArray& message() const; QString get(const QString& key) const; QString post(const QString& key) const; - const QByteArray& torrent() const; + const QList& torrents() const; void writeHeader(const QByteArray& ba); void writeMessage(const QByteArray& ba); inline const QHttpRequestHeader& header() const { return m_header; } @@ -57,7 +57,7 @@ private: QString m_path; QHash m_postMap; QHash m_getMap; - QByteArray m_torrentContent; + QList m_torrents; }; #endif diff --git a/src/webui/scripts/mocha-init.js b/src/webui/scripts/mocha-init.js index c87fe641e..abadd1bc3 100644 --- a/src/webui/scripts/mocha-init.js +++ b/src/webui/scripts/mocha-init.js @@ -3,14 +3,14 @@ ATTACH MOCHA LINK EVENTS Notes: Here is where you define your windows and the events that open them. If you are not using links to run Mocha methods you can remove this function. - + If you need to add link events to links within windows you are creating, do it in the onContentLoaded function of the new window. ----------------------------------------------------------------- */ initializeWindows = function(){ - + function addClickEvent(el, fn){ ['Link','Button'].each(function(item) { if ($(el+item)){ @@ -18,7 +18,7 @@ initializeWindows = function(){ } }); } - + addClickEvent('download', function(e){ new Event(e).stop(); new MochaUI.Window({ @@ -36,7 +36,7 @@ initializeWindows = function(){ height: 300 }); }); - + addClickEvent('preferences', function(e) { new Event(e).stop(); new MochaUI.Window({ @@ -58,7 +58,7 @@ initializeWindows = function(){ height: 300 }); }); - + addClickEvent('upload', function(e){ new Event(e).stop(); new MochaUI.Window({ @@ -72,10 +72,10 @@ initializeWindows = function(){ paddingVertical: 0, paddingHorizontal: 0, width: 600, - height: 170 + height: 130 }); }); - + globalUploadLimitFN = function() { new MochaUI.Window({ id: 'uploadLimitPage', @@ -91,7 +91,7 @@ initializeWindows = function(){ height: 80 }); } - + uploadLimitFN = function() { var h = myTable.selectedIds(); if(h.length){ @@ -111,7 +111,7 @@ initializeWindows = function(){ }); } }; - + globalDownloadLimitFN = function() { new MochaUI.Window({ id: 'downloadLimitPage', @@ -127,7 +127,7 @@ initializeWindows = function(){ height: 80 }); } - + downloadLimitFN = function() { var h = myTable.selectedIds(); if(h.length){ @@ -147,7 +147,7 @@ initializeWindows = function(){ }); } }; - + deleteFN = function() { var h = myTable.selectedIds(); /*if(h.length && confirm('_(Are you sure you want to delete the selected torrents from the transfer list?)')) { @@ -175,7 +175,7 @@ initializeWindows = function(){ new Event(e).stop(); deleteFN(); }); - + pauseFN = function() { var h = myTable.selectedIds(); if(h.length){ @@ -184,7 +184,7 @@ initializeWindows = function(){ }); } }; - + startFN = function() { var h = myTable.selectedIds(); if(h.length){ @@ -193,7 +193,7 @@ initializeWindows = function(){ }); } }; - + recheckFN = function() { var h = myTable.selectedIds(); if(h.length){ @@ -213,7 +213,7 @@ initializeWindows = function(){ }); } }); - + addClickEvent(item+'All', function(e){ new Event(e).stop(); new Request({url: 'command/'+item+'all'}).send(); @@ -226,14 +226,14 @@ initializeWindows = function(){ setPriorityFN(item); }); }); - + setPriorityFN = function(cmd) { var h = myTable.selectedIds(); if(h.length) { new Request({url: 'command/'+cmd, method: 'post', data: {hashes: h.join("|")}}).send(); } } - + addClickEvent('bug', function(e){ new Event(e).stop(); new MochaUI.Window({ @@ -245,7 +245,7 @@ initializeWindows = function(){ height: 400 }); }); - + addClickEvent('site', function(e){ new Event(e).stop(); new MochaUI.Window({ @@ -257,7 +257,7 @@ initializeWindows = function(){ height: 400 }); }); - + addClickEvent('docs', function(e){ new Event(e).stop(); new MochaUI.Window({ @@ -269,7 +269,7 @@ initializeWindows = function(){ height: 400 }); }); - + addClickEvent('about', function(e){ new Event(e).stop(); new MochaUI.Window({ @@ -282,7 +282,7 @@ initializeWindows = function(){ padding: 10 }); }); - + // Deactivate menu header links $$('a.returnFalse').each(function(el){ el.addEvent('click', function(e){