1/* 2 * This file Copyright (C) Mnemosyne LLC 3 * 4 * This program is free software; you can redistribute it and/or modify 5 * it under the terms of the GNU General Public License version 2 6 * as published by the Free Software Foundation. 7 * 8 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html 9 * 10 * $Id: options.cc 13555 2012-10-08 04:23:39Z jordan $ 11 */ 12 13#include <cstdio> 14#include <iostream> 15 16#include <QApplication> 17#include <QCheckBox> 18#include <QComboBox> 19#include <QDialogButtonBox> 20#include <QEvent> 21#include <QFileDialog> 22#include <QFileIconProvider> 23#include <QFileInfo> 24#include <QGridLayout> 25#include <QLabel> 26#include <QMessageBox> 27#include <QPushButton> 28#include <QResizeEvent> 29#include <QSet> 30#include <QVBoxLayout> 31#include <QWidget> 32#include <QLineEdit> 33 34#include <libtransmission/transmission.h> 35#include <libtransmission/bencode.h> 36#include <libtransmission/utils.h> /* mime64 */ 37 38#include "add-data.h" 39#include "file-tree.h" 40#include "hig.h" 41#include "options.h" 42#include "prefs.h" 43#include "session.h" 44#include "torrent.h" 45#include "utils.h" 46 47/*** 48**** 49***/ 50 51void 52FileAdded :: executed( int64_t tag, const QString& result, struct tr_benc * arguments ) 53{ 54 Q_UNUSED( arguments ); 55 56 if( tag != myTag ) 57 return; 58 59 if( ( result == "success" ) && !myDelFile.isEmpty( ) ) { 60 QFile file( myDelFile ); 61 file.setPermissions( QFile::ReadOwner | QFile::WriteOwner ); 62 file.remove(); 63 } 64 65 if( result != "success" ) { 66 QString text = result; 67 for( int i=0, n=text.size(); i<n; ++i ) 68 if( !i || text[i-1].isSpace() ) 69 text[i] = text[i].toUpper(); 70 QMessageBox::warning( QApplication::activeWindow(), 71 tr( "Error Adding Torrent" ), 72 QString("<p><b>%1</b></p><p>%2</p>").arg(text).arg(myName) ); 73 } 74 75 deleteLater(); 76} 77 78/*** 79**** 80***/ 81 82Options :: Options( Session& session, const Prefs& prefs, const AddData& addme, QWidget * parent ): 83 QDialog( parent, Qt::Dialog ), 84 mySession( session ), 85 myAdd( addme ), 86 myHaveInfo( false ), 87 myDestinationButton( 0 ), 88 myVerifyButton( 0 ), 89 myVerifyFile( 0 ), 90 myVerifyHash( QCryptographicHash::Sha1 ) 91 92{ 93 setWindowTitle( tr( "Open Torrent" ) ); 94 QFontMetrics fontMetrics( font( ) ); 95 QGridLayout * layout = new QGridLayout( this ); 96 int row = 0; 97 98 const int iconSize( style( )->pixelMetric( QStyle :: PM_SmallIconSize ) ); 99 QIcon fileIcon = style( )->standardIcon( QStyle::SP_FileIcon ); 100 const QPixmap filePixmap = fileIcon.pixmap( iconSize ); 101 102 QPushButton * p; 103 int width = fontMetrics.size( 0, QString::fromAscii( "This is a pretty long torrent filename indeed.torrent" ) ).width( ); 104 QLabel * l = new QLabel( tr( "&Torrent file:" ) ); 105 layout->addWidget( l, row, 0, Qt::AlignLeft ); 106 p = myFileButton = new QPushButton; 107 p->setIcon( filePixmap ); 108 p->setMinimumWidth( width ); 109 p->setStyleSheet( QString::fromAscii( "text-align: left; padding-left: 5; padding-right: 5" ) ); 110 p->installEventFilter( this ); 111 112 layout->addWidget( p, row, 1 ); 113 l->setBuddy( p ); 114 connect( p, SIGNAL(clicked(bool)), this, SLOT(onFilenameClicked())); 115 116 const QFileIconProvider iconProvider; 117 const QIcon folderIcon = iconProvider.icon( QFileIconProvider::Folder ); 118 const QPixmap folderPixmap = folderIcon.pixmap( iconSize ); 119 120 l = new QLabel( tr( "&Destination folder:" ) ); 121 layout->addWidget( l, ++row, 0, Qt::AlignLeft ); 122 123 if( session.isLocal( ) ) 124 { 125 myDestination.setPath( prefs.getString( Prefs :: DOWNLOAD_DIR ) ); 126 p = myDestinationButton = new QPushButton; 127 p->setIcon( folderPixmap ); 128 p->setStyleSheet( "text-align: left; padding-left: 5; padding-right: 5" ); 129 p->installEventFilter( this ); 130 layout->addWidget( p, row, 1 ); 131 l->setBuddy( p ); 132 connect( p, SIGNAL(clicked(bool)), this, SLOT(onDestinationClicked())); 133 } 134 else 135 { 136 QLineEdit * e = myDestinationEdit = new QLineEdit; 137 e->setText( prefs.getString( Prefs :: DOWNLOAD_DIR ) ); 138 layout->addWidget( e, row, 1 ); 139 l->setBuddy( e ); 140 } 141 142 myTree = new FileTreeView; 143 layout->addWidget( myTree, ++row, 0, 1, 2 ); 144 if( !session.isLocal( ) ) 145 myTree->hideColumn( 1 ); // hide the % done, since we've no way of knowing 146 147 QComboBox * m = new QComboBox; 148 m->addItem( tr( "High" ), TR_PRI_HIGH ); 149 m->addItem( tr( "Normal" ), TR_PRI_NORMAL ); 150 m->addItem( tr( "Low" ), TR_PRI_LOW ); 151 m->setCurrentIndex( 1 ); // Normal 152 myPriorityCombo = m; 153 l = new QLabel( tr( "Torrent &priority:" ) ); 154 l->setBuddy( m ); 155 layout->addWidget( l, ++row, 0, Qt::AlignLeft ); 156 layout->addWidget( m, row, 1 ); 157 158 if( session.isLocal( ) ) 159 { 160 p = myVerifyButton = new QPushButton( tr( "&Verify Local Data" ) ); 161 layout->addWidget( p, ++row, 0, Qt::AlignLeft ); 162 } 163 164 QCheckBox * c; 165 c = myStartCheck = new QCheckBox( tr( "&Start when added" ) ); 166 c->setChecked( prefs.getBool( Prefs :: START ) ); 167 layout->addWidget( c, ++row, 0, 1, 2, Qt::AlignLeft ); 168 169 c = myTrashCheck = new QCheckBox( tr( "Mo&ve .torrent file to the trash" ) ); 170 c->setChecked( prefs.getBool( Prefs :: TRASH_ORIGINAL ) ); 171 layout->addWidget( c, ++row, 0, 1, 2, Qt::AlignLeft ); 172 173 QDialogButtonBox * b = new QDialogButtonBox( QDialogButtonBox::Open|QDialogButtonBox::Cancel, Qt::Horizontal, this ); 174 connect( b, SIGNAL(rejected()), this, SLOT(deleteLater()) ); 175 connect( b, SIGNAL(accepted()), this, SLOT(onAccepted()) ); 176 layout->addWidget( b, ++row, 0, 1, 2 ); 177 178 layout->setRowStretch( 2, 2 ); 179 layout->setColumnStretch( 1, 2 ); 180 layout->setSpacing( HIG :: PAD ); 181 182 connect( myTree, SIGNAL(priorityChanged(const QSet<int>&,int)), this, SLOT(onPriorityChanged(const QSet<int>&,int))); 183 connect( myTree, SIGNAL(wantedChanged(const QSet<int>&,bool)), this, SLOT(onWantedChanged(const QSet<int>&,bool))); 184 if( session.isLocal( ) ) 185 connect( myVerifyButton, SIGNAL(clicked(bool)), this, SLOT(onVerify())); 186 187 connect( &myVerifyTimer, SIGNAL(timeout()), this, SLOT(onTimeout())); 188 189 reload( ); 190} 191 192Options :: ~Options( ) 193{ 194 clearInfo( ); 195} 196 197/*** 198**** 199***/ 200 201void 202Options :: refreshButton( QPushButton * p, const QString& text, int width ) 203{ 204 if( width <= 0 ) width = p->width( ); 205 width -= 15; 206 QFontMetrics fontMetrics( font( ) ); 207 QString str = fontMetrics.elidedText( text, Qt::ElideRight, width ); 208 p->setText( str ); 209} 210 211void 212Options :: refreshFileButton( int width ) 213{ 214 QString text; 215 216 switch( myAdd.type ) 217 { 218 case AddData::FILENAME: text = QFileInfo(myAdd.filename).baseName(); break; 219 case AddData::URL: text = myAdd.url.toString(); break; 220 case AddData::MAGNET: text = myAdd.magnet; break; 221 default: break; 222 } 223 224 refreshButton( myFileButton, text, width ); 225} 226 227void 228Options :: refreshDestinationButton( int width ) 229{ 230 if( myDestinationButton != 0 ) 231 refreshButton( myDestinationButton, myDestination.absolutePath(), width ); 232} 233 234 235bool 236Options :: eventFilter( QObject * o, QEvent * event ) 237{ 238 if( o==myFileButton && event->type() == QEvent::Resize ) 239 { 240 refreshFileButton( dynamic_cast<QResizeEvent*>(event)->size().width() ); 241 } 242 243 if( o==myDestinationButton && event->type() == QEvent::Resize ) 244 { 245 refreshDestinationButton( dynamic_cast<QResizeEvent*>(event)->size().width() ); 246 } 247 248 return false; 249} 250 251/*** 252**** 253***/ 254 255void 256Options :: clearInfo( ) 257{ 258 if( myHaveInfo ) 259 tr_metainfoFree( &myInfo ); 260 myHaveInfo = false; 261 myFiles.clear( ); 262} 263 264void 265Options :: reload( ) 266{ 267 clearInfo( ); 268 clearVerify( ); 269 270 tr_ctor * ctor = tr_ctorNew( 0 ); 271 272 switch( myAdd.type ) { 273 case AddData::MAGNET: tr_ctorSetMetainfoFromMagnetLink( ctor, myAdd.magnet.toUtf8().constData() ); break; 274 case AddData::FILENAME: tr_ctorSetMetainfoFromFile( ctor, myAdd.filename.toUtf8().constData() ); break; 275 case AddData::METAINFO: tr_ctorSetMetainfo( ctor, (const uint8_t*)myAdd.metainfo.constData(), myAdd.metainfo.size() ); break; 276 default: break; 277 } 278 279 const int err = tr_torrentParse( ctor, &myInfo ); 280 myHaveInfo = !err; 281 tr_ctorFree( ctor ); 282 283 myTree->clear( ); 284 myFiles.clear( ); 285 myPriorities.clear( ); 286 myWanted.clear( ); 287 288 if( myHaveInfo ) 289 { 290 myPriorities.insert( 0, myInfo.fileCount, TR_PRI_NORMAL ); 291 myWanted.insert( 0, myInfo.fileCount, true ); 292 293 for( tr_file_index_t i=0; i<myInfo.fileCount; ++i ) { 294 TrFile file; 295 file.index = i; 296 file.priority = myPriorities[i]; 297 file.wanted = myWanted[i]; 298 file.size = myInfo.files[i].length; 299 file.have = 0; 300 file.filename = QString::fromUtf8( myInfo.files[i].name ); 301 myFiles.append( file ); 302 } 303 } 304 305 myTree->update( myFiles ); 306} 307 308void 309Options :: onPriorityChanged( const QSet<int>& fileIndices, int priority ) 310{ 311 foreach( int i, fileIndices ) 312 myPriorities[i] = priority; 313} 314 315void 316Options :: onWantedChanged( const QSet<int>& fileIndices, bool isWanted ) 317{ 318 foreach( int i, fileIndices ) 319 myWanted[i] = isWanted; 320} 321 322void 323Options :: onAccepted( ) 324{ 325 // rpc spec section 3.4 "adding a torrent" 326 327 const int64_t tag = mySession.getUniqueTag( ); 328 tr_benc top; 329 tr_bencInitDict( &top, 3 ); 330 tr_bencDictAddStr( &top, "method", "torrent-add" ); 331 tr_bencDictAddInt( &top, "tag", tag ); 332 tr_benc * args( tr_bencDictAddDict( &top, "arguments", 10 ) ); 333 QString downloadDir; 334 335 // "download-dir" 336 if( myDestinationButton ) 337 downloadDir = myDestination.absolutePath(); 338 else 339 downloadDir = myDestinationEdit->text(); 340 tr_bencDictAddStr( args, "download-dir", downloadDir.toUtf8().constData() ); 341 342 // "metainfo" 343 switch( myAdd.type ) 344 { 345 case AddData::MAGNET: 346 tr_bencDictAddStr( args, "filename", myAdd.magnet.toUtf8().constData() ); 347 break; 348 349 case AddData::URL: 350 tr_bencDictAddStr( args, "filename", myAdd.url.toString().toUtf8().constData() ); 351 break; 352 353 case AddData::FILENAME: 354 case AddData::METAINFO: { 355 const QByteArray b64 = myAdd.toBase64( ); 356 tr_bencDictAddRaw( args, "metainfo", b64.constData(), b64.size() ); 357 break; 358 } 359 360 default: 361 std::cerr << "unhandled AddData.type: " << myAdd.type << std::endl; 362 } 363 364 // paused 365 tr_bencDictAddBool( args, "paused", !myStartCheck->isChecked( ) ); 366 367 // priority 368 const int index = myPriorityCombo->currentIndex( ); 369 const int priority = myPriorityCombo->itemData(index).toInt( ); 370 tr_bencDictAddInt( args, "bandwidthPriority", priority ); 371 372 // files-unwanted 373 int count = myWanted.count( false ); 374 if( count > 0 ) { 375 tr_benc * l = tr_bencDictAddList( args, "files-unwanted", count ); 376 for( int i=0, n=myWanted.size(); i<n; ++i ) 377 if( myWanted.at(i) == false ) 378 tr_bencListAddInt( l, i ); 379 } 380 381 // priority-low 382 count = myPriorities.count( TR_PRI_LOW ); 383 if( count > 0 ) { 384 tr_benc * l = tr_bencDictAddList( args, "priority-low", count ); 385 for( int i=0, n=myPriorities.size(); i<n; ++i ) 386 if( myPriorities.at(i) == TR_PRI_LOW ) 387 tr_bencListAddInt( l, i ); 388 } 389 390 // priority-high 391 count = myPriorities.count( TR_PRI_HIGH ); 392 if( count > 0 ) { 393 tr_benc * l = tr_bencDictAddList( args, "priority-high", count ); 394 for( int i=0, n=myPriorities.size(); i<n; ++i ) 395 if( myPriorities.at(i) == TR_PRI_HIGH ) 396 tr_bencListAddInt( l, i ); 397 } 398 399 // maybe delete the source .torrent 400 FileAdded * fileAdded = new FileAdded( tag, myAdd.readableName() ); 401 if( myTrashCheck->isChecked( ) && ( myAdd.type==AddData::FILENAME ) ) 402 fileAdded->setFileToDelete( myAdd.filename ); 403 connect( &mySession, SIGNAL(executed(int64_t,const QString&, struct tr_benc*)), 404 fileAdded, SLOT(executed(int64_t,const QString&, struct tr_benc*))); 405 406//std::cerr << tr_bencToStr(&top,TR_FMT_JSON,NULL) << std::endl; 407 mySession.exec( &top ); 408 409 tr_bencFree( &top ); 410 deleteLater( ); 411} 412 413void 414Options :: onFilenameClicked( ) 415{ 416 if( myAdd.type == AddData::FILENAME ) 417 { 418 QFileDialog * d = new QFileDialog( this, 419 tr( "Open Torrent" ), 420 QFileInfo(myAdd.filename).absolutePath(), 421 tr( "Torrent Files (*.torrent);;All Files (*.*)" ) ); 422 d->setFileMode( QFileDialog::ExistingFile ); 423 d->setAttribute( Qt::WA_DeleteOnClose ); 424 connect( d, SIGNAL(filesSelected(const QStringList&)), this, SLOT(onFilesSelected(const QStringList&)) ); 425 d->show( ); 426 } 427} 428 429void 430Options :: onFilesSelected( const QStringList& files ) 431{ 432 if( files.size() == 1 ) 433 { 434 myAdd.set( files.at(0) ); 435 refreshFileButton( ); 436 reload( ); 437 } 438} 439 440void 441Options :: onDestinationClicked( ) 442{ 443 QFileDialog * d = new QFileDialog( this, 444 tr( "Select Destination" ), 445 myDestination.absolutePath( ) ); 446 d->setFileMode( QFileDialog::Directory ); 447 d->setAttribute( Qt::WA_DeleteOnClose ); 448 connect( d, SIGNAL(filesSelected(const QStringList&)), this, SLOT(onDestinationsSelected(const QStringList&)) ); 449 d->show( ); 450} 451 452void 453Options :: onDestinationsSelected( const QStringList& destinations ) 454{ 455 if( destinations.size() == 1 ) 456 { 457 const QString& destination( destinations.first( ) ); 458 myDestination.setPath( destination ); 459 refreshDestinationButton( ); 460 } 461} 462 463/*** 464**** 465**** VERIFY 466**** 467***/ 468 469void 470Options :: clearVerify( ) 471{ 472 myVerifyHash.reset( ); 473 myVerifyFile.close( ); 474 myVerifyFilePos = 0; 475 myVerifyFlags.clear( ); 476 myVerifyFileIndex = 0; 477 myVerifyPieceIndex = 0; 478 myVerifyPiecePos = 0; 479 myVerifyTimer.stop( ); 480 481 for( int i=0, n=myFiles.size(); i<n; ++i ) 482 myFiles[i].have = 0; 483 myTree->update( myFiles ); 484} 485 486void 487Options :: onVerify( ) 488{ 489 //std::cerr << "starting to verify..." << std::endl; 490 clearVerify( ); 491 myVerifyFlags.insert( 0, myInfo.pieceCount, false ); 492 myVerifyTimer.setSingleShot( false ); 493 myVerifyTimer.start( 0 ); 494} 495 496namespace 497{ 498 uint64_t getPieceSize( const tr_info * info, tr_piece_index_t pieceIndex ) 499 { 500 if( pieceIndex != info->pieceCount - 1 ) 501 return info->pieceSize; 502 return info->totalSize % info->pieceSize; 503 } 504} 505 506void 507Options :: onTimeout( ) 508{ 509 const tr_file * file = &myInfo.files[myVerifyFileIndex]; 510 511 if( !myVerifyFilePos && !myVerifyFile.isOpen( ) ) 512 { 513 const QFileInfo fileInfo( myDestination, QString::fromUtf8( file->name ) ); 514 myVerifyFile.setFileName( fileInfo.absoluteFilePath( ) ); 515 //std::cerr << "opening file" << qPrintable(fileInfo.absoluteFilePath()) << std::endl; 516 myVerifyFile.open( QIODevice::ReadOnly ); 517 } 518 519 int64_t leftInPiece = getPieceSize( &myInfo, myVerifyPieceIndex ) - myVerifyPiecePos; 520 int64_t leftInFile = file->length - myVerifyFilePos; 521 int64_t bytesThisPass = std::min( leftInFile, leftInPiece ); 522 bytesThisPass = std::min( bytesThisPass, (int64_t)sizeof( myVerifyBuf ) ); 523 524 if( myVerifyFile.isOpen() && myVerifyFile.seek( myVerifyFilePos ) ) { 525 int64_t numRead = myVerifyFile.read( myVerifyBuf, bytesThisPass ); 526 if( numRead == bytesThisPass ) 527 myVerifyHash.addData( myVerifyBuf, numRead ); 528 } 529 530 leftInPiece -= bytesThisPass; 531 leftInFile -= bytesThisPass; 532 myVerifyPiecePos += bytesThisPass; 533 myVerifyFilePos += bytesThisPass; 534 535 myVerifyBins[myVerifyFileIndex] += bytesThisPass; 536 537 if( leftInPiece == 0 ) 538 { 539 const QByteArray result( myVerifyHash.result( ) ); 540 const bool matches = !memcmp( result.constData(), 541 myInfo.pieces[myVerifyPieceIndex].hash, 542 SHA_DIGEST_LENGTH ); 543 myVerifyFlags[myVerifyPieceIndex] = matches; 544 myVerifyPiecePos = 0; 545 ++myVerifyPieceIndex; 546 myVerifyHash.reset( ); 547 548 FileList changedFiles; 549 if( matches ) { 550 mybins_t::const_iterator i; 551 for( i=myVerifyBins.begin(); i!=myVerifyBins.end(); ++i ) { 552 TrFile& f( myFiles[i.key( )] ); 553 f.have += i.value( ); 554 changedFiles.append( f ); 555 } 556 } 557 myTree->update( changedFiles ); 558 myVerifyBins.clear( ); 559 } 560 561 if( leftInFile == 0 ) 562 { 563 //std::cerr << "closing file" << std::endl; 564 myVerifyFile.close( ); 565 ++myVerifyFileIndex; 566 myVerifyFilePos = 0; 567 } 568 569 bool done = myVerifyPieceIndex >= myInfo.pieceCount; 570 if( done ) 571 { 572 uint64_t have = 0; 573 foreach( const TrFile& f, myFiles ) 574 have += f.have; 575 576 if( !have ) // everything failed 577 { 578 // did the user accidentally specify the child directory instead of the parent? 579 const QStringList tokens = QString(file->name).split('/'); 580 if( !tokens.empty() && myDestination.dirName()==tokens.at(0) ) 581 { 582 // move up one directory and try again 583 myDestination.cdUp( ); 584 refreshDestinationButton( -1 ); 585 onVerify( ); 586 done = false; 587 } 588 } 589 } 590 591 if( done ) 592 myVerifyTimer.stop( ); 593} 594