mirror of
https://github.com/d47081/qBittorrent.git
synced 2025-01-22 12:34:19 +00:00
Add support for adding multiple local torrents at once (Web UI)
This commit is contained in:
parent
6d31af676e
commit
a98ad63d8b
@ -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
|
||||||
|
@ -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>
|
||||||
|
@ -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");
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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…
x
Reference in New Issue
Block a user