mirror of
https://github.com/qbittorrent/qBittorrent.git
synced 2024-11-23 09:49:52 +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
|
||||
- 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
|
||||
|
@ -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>
|
||||
|
@ -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");
|
||||
|
@ -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) {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
**/
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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:
|
||||
QString m_path;
|
||||
QHash<QString, QString> m_postMap;
|
||||
QHash<QString, QString> m_getMap;
|
||||
QByteArray m_torrentContent;
|
||||
QList<QByteArray> m_torrents;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
@ -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){
|
||||
|
Loading…
Reference in New Issue
Block a user