Browse Source

Add support for adding multiple local torrents at once (Web UI)

adaptive-webui-19844
Christophe Dumez 13 years ago
parent
commit
a98ad63d8b
  1. 1
      Changelog
  2. 49
      src/webui/html/upload.html
  3. 37
      src/webui/httpconnection.cpp
  4. 50
      src/webui/httprequestparser.cpp
  5. 4
      src/webui/httprequestparser.h
  6. 42
      src/webui/scripts/mocha-init.js

1
Changelog

@ -1,6 +1,7 @@
* Unreleased - Christophe Dumez <chris@qbittorrent.org> - v3.0.0 * Unreleased - Christophe Dumez <chris@qbittorrent.org> - v3.0.0
- FEATURE: Brand new torrent addition dialog - FEATURE: Brand new torrent addition dialog
- FEATURE: Add the ability to choose the save path when using magnet links (mutoso) - 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 - COSMETIC: Improve style of left panel
- BUGFIX: Lower panels no longer gets disabled - BUGFIX: Lower panels no longer gets disabled
- OTHER: Drop support for libtorrent v0.14.x - OTHER: Drop support for libtorrent v0.14.x

49
src/webui/html/upload.html

@ -6,19 +6,58 @@
<link rel="stylesheet" href="css/style.css" type="text/css" /> <link rel="stylesheet" href="css/style.css" type="text/css" />
<script type="text/javascript" src="scripts/mootools-1.2-core-yc.js" charset="utf-8"></script> <script type="text/javascript" src="scripts/mootools-1.2-core-yc.js" charset="utf-8"></script>
<script type="text/javascript"> <script type="text/javascript">
function hideAll() { function hideAll() {
window.parent.closeWindows(); window.parent.closeWindows();
} }
function uploadFiles(files) {
var xhr = new XMLHttpRequest();
if (xhr.upload) {
// file received/failed
xhr.onreadystatechange = function(e) {
if (xhr.readyState == 4) {
if (xhr.status == 200)
hideAll();
else
alert("Upload Failed!");
}
};
// start upload
var formData = new FormData();
for (var i = 0, file; file = files[i]; ++i) {
formData.append(file.name, file);
}
xhr.open("POST", "command/upload", true);
xhr.send(formData);
}
}
// file selection
function fileSelectHandler(e) {
// fetch FileList object
var files = e.target.files || e.dataTransfer.files;
// process all File objects
uploadFiles(files);
}
window.addEvent('load', function() {
$('fileselect').addEvent('change', fileSelectHandler);
// is XHR2 available?
var xhr = new XMLHttpRequest();
if (xhr.upload) {
$('submitbutton').addClass("invisible");
}
});
</script> </script>
</head> </head>
<body> <body>
<br/>
<iframe id="upload_frame" name="upload_frame" style="width:1px;height:1px;border:0px;" src="javascript:false;"></iframe> <iframe id="upload_frame" name="upload_frame" style="width:1px;height:1px;border:0px;" src="javascript:false;"></iframe>
<center>
<h1 class="vcenter"><img class="vcenter" title="Download local torrent" src="theme/list-add"/>_(Download local torrent)</h1>
<form action="command/upload" enctype="multipart/form-data" method="post" id="uploadForm" target="upload_frame"> <form action="command/upload" enctype="multipart/form-data" method="post" id="uploadForm" target="upload_frame">
<input type="file" name="torrentfile" id="torrentfile" size="40"/><br/><br/> <input type="file" id="fileselect" name="fileselect[]" multiple="multiple" /><br/>
<input type="submit" value="_(Download)" id="upButton"/> <div id="submitbutton">
<button type="submit">_(Upload Torrents)</button>
</div
</form> </form>
</center>
</body> </body>
</html> </html>

37
src/webui/httpconnection.cpp

@ -419,23 +419,26 @@ void HttpConnection::respondCommand(const QString& command) {
} }
if (command == "upload") { if (command == "upload") {
qDebug() << Q_FUNC_INFO << "upload"; qDebug() << Q_FUNC_INFO << "upload";
// Get a unique filename const QList<QByteArray>& torrents = m_parser.torrents();
QTemporaryFile *tmpfile = new QTemporaryFile (QDir::temp().absoluteFilePath("qBT-XXXXXX.torrent")); foreach(const QByteArray& torrentContent, torrents) {
tmpfile->setAutoRemove(false); // Get a unique filename
if (tmpfile->open()) { QTemporaryFile *tmpfile = new QTemporaryFile (QDir::temp().absoluteFilePath("qBT-XXXXXX.torrent"));
QString filePath = tmpfile->fileName(); tmpfile->setAutoRemove(false);
tmpfile->write(m_parser.torrent()); if (tmpfile->open()) {
tmpfile->close(); QString filePath = tmpfile->fileName();
// XXX: tmpfile needs to be deleted on Windows before using the file tmpfile->write(torrentContent);
// or it will complain that the file is used by another process. tmpfile->close();
delete tmpfile; // XXX: tmpfile needs to be deleted on Windows before using the file
emit torrentReadyToBeDownloaded(filePath, false, QString(), false); // or it will complain that the file is used by another process.
// Clean up delete tmpfile;
QFile::remove(filePath); emit torrentReadyToBeDownloaded(filePath, false, QString(), false);
} else { // Clean up
std::cerr << "I/O Error: Could not create temporary file" << std::endl; QFile::remove(filePath);
delete tmpfile; } else {
return; std::cerr << "I/O Error: Could not create temporary file" << std::endl;
delete tmpfile;
return;
}
} }
// Prepare response // Prepare response
m_generator.setStatusLine(200, "OK"); m_generator.setStatusLine(200, "OK");

50
src/webui/httprequestparser.cpp

@ -61,8 +61,8 @@ QString HttpRequestParser::post(const QString& key) const {
return m_postMap.value(key); return m_postMap.value(key);
} }
const QByteArray& HttpRequestParser::torrent() const { const QList<QByteArray>& HttpRequestParser::torrents() const {
return m_torrentContent; return m_torrents;
} }
void HttpRequestParser::writeHeader(const QByteArray& ba) { void HttpRequestParser::writeHeader(const QByteArray& ba) {
@ -79,6 +79,18 @@ void HttpRequestParser::writeHeader(const QByteArray& ba) {
} }
} }
static QList<QByteArray> splitRawData(QByteArray rawData, const QByteArray& sep)
{
QList<QByteArray> 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) { void HttpRequestParser::writeMessage(const QByteArray& ba) {
// Parse message content // Parse message content
Q_ASSERT (m_header.hasContentLength()); Q_ASSERT (m_header.hasContentLength());
@ -119,21 +131,29 @@ Submit Query
**/ **/
if (m_header.contentType().startsWith("multipart/form-data")) { if (m_header.contentType().startsWith("multipart/form-data")) {
qDebug() << Q_FUNC_INFO << "header is: " << m_header.toString(); qDebug() << Q_FUNC_INFO << "header is: " << m_header.toString();
static QRegExp boundaryRegexQuoted("boundary=\"([ \\w'()+,-\\./:=\\?]+)\"");
int filename_index = m_data.indexOf("filename="); static QRegExp boundaryRegexNotQuoted("boundary=([\\w'()+,-\\./:=\\?]+)");
if (filename_index >= 0) { QByteArray boundary;
QByteArray boundary = m_data.left(m_data.indexOf("\r\n")); if (boundaryRegexQuoted.indexIn(m_header.toString()) < 0) {
qDebug() << "Boundary is " << boundary << "\n\n"; if (boundaryRegexNotQuoted.indexIn(m_header.toString()) < 0) {
qDebug() << "Before binary data: " << m_data.left(m_data.indexOf("\r\n\r\n", filename_index+9)) << "\n\n"; qWarning() << "Could not find boundary in multipart/form-data header!";
m_torrentContent = m_data.mid(m_data.indexOf("\r\n\r\n", filename_index+9) + 4); m_error = true;
int binaryend_index = m_torrentContent.indexOf("\r\n"+boundary); return;
if (binaryend_index >= 0) { } else {
qDebug() << "found end boundary :)"; boundary = "--" + boundaryRegexNotQuoted.cap(1).toAscii();
m_torrentContent = m_torrentContent.left(binaryend_index);
} }
qDebug() << Q_FUNC_INFO << "m_torrentContent.size(): " << m_torrentContent.size()<< "\n\n";
} else { } else {
m_error = true; boundary = "--" + boundaryRegexQuoted.cap(1).toAscii();
}
qDebug() << "Boundary is " << boundary;
QList<QByteArray> 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);
} }
} }
} }

4
src/webui/httprequestparser.h

@ -45,7 +45,7 @@ public:
const QByteArray& message() const; const QByteArray& message() const;
QString get(const QString& key) const; QString get(const QString& key) const;
QString post(const QString& key) const; QString post(const QString& key) const;
const QByteArray& torrent() const; const QList<QByteArray>& torrents() const;
void writeHeader(const QByteArray& ba); void writeHeader(const QByteArray& ba);
void writeMessage(const QByteArray& ba); void writeMessage(const QByteArray& ba);
inline const QHttpRequestHeader& header() const { return m_header; } inline const QHttpRequestHeader& header() const { return m_header; }
@ -57,7 +57,7 @@ private:
QString m_path; QString m_path;
QHash<QString, QString> m_postMap; QHash<QString, QString> m_postMap;
QHash<QString, QString> m_getMap; QHash<QString, QString> m_getMap;
QByteArray m_torrentContent; QList<QByteArray> m_torrents;
}; };
#endif #endif

42
src/webui/scripts/mocha-init.js

@ -3,14 +3,14 @@
ATTACH MOCHA LINK EVENTS ATTACH MOCHA LINK EVENTS
Notes: Here is where you define your windows and the events that open them. 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 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 If you need to add link events to links within windows you are creating, do
it in the onContentLoaded function of the new window. it in the onContentLoaded function of the new window.
----------------------------------------------------------------- */ ----------------------------------------------------------------- */
initializeWindows = function(){ initializeWindows = function(){
function addClickEvent(el, fn){ function addClickEvent(el, fn){
['Link','Button'].each(function(item) { ['Link','Button'].each(function(item) {
if ($(el+item)){ if ($(el+item)){
@ -18,7 +18,7 @@ initializeWindows = function(){
} }
}); });
} }
addClickEvent('download', function(e){ addClickEvent('download', function(e){
new Event(e).stop(); new Event(e).stop();
new MochaUI.Window({ new MochaUI.Window({
@ -36,7 +36,7 @@ initializeWindows = function(){
height: 300 height: 300
}); });
}); });
addClickEvent('preferences', function(e) { addClickEvent('preferences', function(e) {
new Event(e).stop(); new Event(e).stop();
new MochaUI.Window({ new MochaUI.Window({
@ -58,7 +58,7 @@ initializeWindows = function(){
height: 300 height: 300
}); });
}); });
addClickEvent('upload', function(e){ addClickEvent('upload', function(e){
new Event(e).stop(); new Event(e).stop();
new MochaUI.Window({ new MochaUI.Window({
@ -72,10 +72,10 @@ initializeWindows = function(){
paddingVertical: 0, paddingVertical: 0,
paddingHorizontal: 0, paddingHorizontal: 0,
width: 600, width: 600,
height: 170 height: 130
}); });
}); });
globalUploadLimitFN = function() { globalUploadLimitFN = function() {
new MochaUI.Window({ new MochaUI.Window({
id: 'uploadLimitPage', id: 'uploadLimitPage',
@ -91,7 +91,7 @@ initializeWindows = function(){
height: 80 height: 80
}); });
} }
uploadLimitFN = function() { uploadLimitFN = function() {
var h = myTable.selectedIds(); var h = myTable.selectedIds();
if(h.length){ if(h.length){
@ -111,7 +111,7 @@ initializeWindows = function(){
}); });
} }
}; };
globalDownloadLimitFN = function() { globalDownloadLimitFN = function() {
new MochaUI.Window({ new MochaUI.Window({
id: 'downloadLimitPage', id: 'downloadLimitPage',
@ -127,7 +127,7 @@ initializeWindows = function(){
height: 80 height: 80
}); });
} }
downloadLimitFN = function() { downloadLimitFN = function() {
var h = myTable.selectedIds(); var h = myTable.selectedIds();
if(h.length){ if(h.length){
@ -147,7 +147,7 @@ initializeWindows = function(){
}); });
} }
}; };
deleteFN = function() { deleteFN = function() {
var h = myTable.selectedIds(); var h = myTable.selectedIds();
/*if(h.length && confirm('_(Are you sure you want to delete the selected torrents from the transfer list?)')) { /*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(); new Event(e).stop();
deleteFN(); deleteFN();
}); });
pauseFN = function() { pauseFN = function() {
var h = myTable.selectedIds(); var h = myTable.selectedIds();
if(h.length){ if(h.length){
@ -184,7 +184,7 @@ initializeWindows = function(){
}); });
} }
}; };
startFN = function() { startFN = function() {
var h = myTable.selectedIds(); var h = myTable.selectedIds();
if(h.length){ if(h.length){
@ -193,7 +193,7 @@ initializeWindows = function(){
}); });
} }
}; };
recheckFN = function() { recheckFN = function() {
var h = myTable.selectedIds(); var h = myTable.selectedIds();
if(h.length){ if(h.length){
@ -213,7 +213,7 @@ initializeWindows = function(){
}); });
} }
}); });
addClickEvent(item+'All', function(e){ addClickEvent(item+'All', function(e){
new Event(e).stop(); new Event(e).stop();
new Request({url: 'command/'+item+'all'}).send(); new Request({url: 'command/'+item+'all'}).send();
@ -226,14 +226,14 @@ initializeWindows = function(){
setPriorityFN(item); setPriorityFN(item);
}); });
}); });
setPriorityFN = function(cmd) { setPriorityFN = function(cmd) {
var h = myTable.selectedIds(); var h = myTable.selectedIds();
if(h.length) { if(h.length) {
new Request({url: 'command/'+cmd, method: 'post', data: {hashes: h.join("|")}}).send(); new Request({url: 'command/'+cmd, method: 'post', data: {hashes: h.join("|")}}).send();
} }
} }
addClickEvent('bug', function(e){ addClickEvent('bug', function(e){
new Event(e).stop(); new Event(e).stop();
new MochaUI.Window({ new MochaUI.Window({
@ -245,7 +245,7 @@ initializeWindows = function(){
height: 400 height: 400
}); });
}); });
addClickEvent('site', function(e){ addClickEvent('site', function(e){
new Event(e).stop(); new Event(e).stop();
new MochaUI.Window({ new MochaUI.Window({
@ -257,7 +257,7 @@ initializeWindows = function(){
height: 400 height: 400
}); });
}); });
addClickEvent('docs', function(e){ addClickEvent('docs', function(e){
new Event(e).stop(); new Event(e).stop();
new MochaUI.Window({ new MochaUI.Window({
@ -269,7 +269,7 @@ initializeWindows = function(){
height: 400 height: 400
}); });
}); });
addClickEvent('about', function(e){ addClickEvent('about', function(e){
new Event(e).stop(); new Event(e).stop();
new MochaUI.Window({ new MochaUI.Window({
@ -282,7 +282,7 @@ initializeWindows = function(){
padding: 10 padding: 10
}); });
}); });
// Deactivate menu header links // Deactivate menu header links
$$('a.returnFalse').each(function(el){ $$('a.returnFalse').each(function(el){
el.addEvent('click', function(e){ el.addEvent('click', function(e){

Loading…
Cancel
Save