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 @@ @@ -1,6 +1,7 @@
* Unreleased - Christophe Dumez <chris@qbittorrent.org> - 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

49
src/webui/html/upload.html

@ -6,19 +6,58 @@ @@ -6,19 +6,58 @@
<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">
function hideAll() {
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>
</head>
<body>
<br/>
<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">
<input type="file" name="torrentfile" id="torrentfile" size="40"/><br/><br/>
<input type="submit" value="_(Download)" id="upButton"/>
<input type="file" id="fileselect" name="fileselect[]" multiple="multiple" /><br/>
<div id="submitbutton">
<button type="submit">_(Upload Torrents)</button>
</div
</form>
</center>
</body>
</html>

37
src/webui/httpconnection.cpp

@ -419,23 +419,26 @@ void HttpConnection::respondCommand(const QString& command) { @@ -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<QByteArray>& 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");

50
src/webui/httprequestparser.cpp

@ -61,8 +61,8 @@ QString HttpRequestParser::post(const QString& key) const { @@ -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<QByteArray>& HttpRequestParser::torrents() const {
return m_torrents;
}
void HttpRequestParser::writeHeader(const QByteArray& ba) {
@ -79,6 +79,18 @@ 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) {
// Parse message content
Q_ASSERT (m_header.hasContentLength());
@ -119,21 +131,29 @@ Submit Query @@ -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<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: @@ -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<QByteArray>& 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: @@ -57,7 +57,7 @@ private:
QString m_path;
QHash<QString, QString> m_postMap;
QHash<QString, QString> m_getMap;
QByteArray m_torrentContent;
QList<QByteArray> m_torrents;
};
#endif

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

@ -3,14 +3,14 @@ @@ -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(){ @@ -18,7 +18,7 @@ initializeWindows = function(){
}
});
}
addClickEvent('download', function(e){
new Event(e).stop();
new MochaUI.Window({
@ -36,7 +36,7 @@ initializeWindows = function(){ @@ -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(){ @@ -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(){ @@ -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(){ @@ -91,7 +91,7 @@ initializeWindows = function(){
height: 80
});
}
uploadLimitFN = function() {
var h = myTable.selectedIds();
if(h.length){
@ -111,7 +111,7 @@ initializeWindows = function(){ @@ -111,7 +111,7 @@ initializeWindows = function(){
});
}
};
globalDownloadLimitFN = function() {
new MochaUI.Window({
id: 'downloadLimitPage',
@ -127,7 +127,7 @@ initializeWindows = function(){ @@ -127,7 +127,7 @@ initializeWindows = function(){
height: 80
});
}
downloadLimitFN = function() {
var h = myTable.selectedIds();
if(h.length){
@ -147,7 +147,7 @@ initializeWindows = function(){ @@ -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(){ @@ -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(){ @@ -184,7 +184,7 @@ initializeWindows = function(){
});
}
};
startFN = function() {
var h = myTable.selectedIds();
if(h.length){
@ -193,7 +193,7 @@ initializeWindows = function(){ @@ -193,7 +193,7 @@ initializeWindows = function(){
});
}
};
recheckFN = function() {
var h = myTable.selectedIds();
if(h.length){
@ -213,7 +213,7 @@ initializeWindows = function(){ @@ -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(){ @@ -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(){ @@ -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(){ @@ -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(){ @@ -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(){ @@ -282,7 +282,7 @@ initializeWindows = function(){
padding: 10
});
});
// Deactivate menu header links
$$('a.returnFalse').each(function(el){
el.addEvent('click', function(e){

Loading…
Cancel
Save