Package ts3widgets :: Module filetransfer
[hide private]
[frames] | no frames]

Source Code for Module ts3widgets.filetransfer

   1  import os 
   2   
   3  from . import _errprint 
   4   
   5  import ts3lib 
   6  from ts3defines import (FileListType, ERROR_ok, ERROR_database_empty_result, 
   7                          ERROR_file_transfer_complete, ChannelProperties) 
   8  import pytson 
   9  from pytsonui import setupUi 
  10  import ts3client 
  11   
  12  from pluginhost import PluginHost 
  13  from signalslot import Signal 
  14   
  15  from PythonQt.QtCore import (Qt, QAbstractItemModel, QModelIndex, QUrl) 
  16  from PythonQt.QtGui import (QDialog, QStyledItemDelegate, QIcon, QHeaderView, 
  17                              QSortFilterProxyModel, QFileDialog, QLineEdit, 
  18                              QInputDialog, QStatusBar, QMessageBox, 
  19                              QStyleOptionProgressBar, QApplication, QStyle, 
  20                              QDesktopServices, QMenu) 
  21  from PythonQt import BoolResult 
  22   
  23  from datetime import datetime 
  24   
  25  from collections import OrderedDict 
26 27 28 -def splitpath(path):
29 """ 30 Splits a TS3 filepath into its sections. 31 @param path: the path to split 32 @type path: str 33 @return: the list of sections 34 @rtype: list[str] 35 """ 36 return ["/"] + list(filter(None, path.split('/')))
37
38 39 -def joinpath(*args):
40 """ 41 Joins multiple sections into a TS3 filepath. 42 @param args: sections to join 43 @type args: tuple(str) 44 @return: the resulting path 45 @rtype: str 46 """ 47 return "/" + "/".join(filter(lambda x: x not in ["/", ""], args))
48
49 50 -def bytesToStr(size):
51 """ 52 Creates a human readable string of a number of bytes. 53 @param size: number of bytes 54 @type size: int 55 @return: the converted size and most fitting unit 56 @rtype: str 57 """ 58 bias = 1024.0 59 units = ['B', 'KiB', 'MiB', 'GiB', 'TiB'] 60 61 for u in units: 62 if abs(size) < bias: 63 return "%3.2f %s" % (size, u) 64 size /= bias 65 66 return "%3.2f PiB" % size
67
68 69 -class File(object):
70 """ 71 Container class to hold all information on a remote TS3 file. 72 """ 73
74 - def __init__(self, path, name, size, date, atype, incompletesize):
75 self.path = path 76 self.name = name 77 self.size = size 78 self.datetime = datetime.fromtimestamp(date) 79 self.type = atype 80 self.incompletesize = incompletesize
81 82 @property
83 - def isDirectory(self):
84 return self.type == FileListType.FileListType_Directory
85 86 @property
87 - def icon(self):
88 """ 89 Returns the most fitting icon for the file 90 @return: the icon 91 @rtype: QIcon 92 """ 93 if self.isDirectory: 94 return QIcon.fromTheme("folder") 95 else: 96 ext = os.path.splitext(self.name)[-1][1:].lower() 97 theme = None 98 99 if ext in ['exe', 'dll', 'bat', 'dylib', 'sh', 'run']: 100 theme = "application-x-executable" 101 elif ext in ['mp3', 'ogg', 'wav', 'wma', 'flac']: 102 theme = "audio-x-generic" 103 elif ext in ['ttf', 'woff', 'eot']: 104 theme = "font-x-generic" 105 elif ext in ['png', 'jpg', 'jpeg', 'gif', 'svg', ' bmp']: 106 theme = "image-x-generic" 107 elif ext in ['avi', 'wmv', 'qt', 'mkv', 'flv', 'mpg', 'ram', 'mov', 108 'mp4']: 109 theme = "video-x-generic" 110 111 if not theme: 112 theme = "text-x-generic" 113 114 return QIcon.fromTheme(theme)
115 116 @property
117 - def fullpath(self):
118 return joinpath(self.path, self.name)
119
120 121 -class FileListModel(QAbstractItemModel, pytson.Translatable):
122 """ 123 Itemmodel to abstract the files contained on a TS3 filepath. 124 """ 125
126 - def __init__(self, schid, cid, password, parent=None, readonly=False):
127 super(QAbstractItemModel, self).__init__(parent) 128 129 self.schid = schid 130 self.cid = cid 131 self.password = password 132 133 self.readonly = readonly 134 135 self.pathChanged = Signal() 136 self.error = Signal() 137 138 self._path = None 139 self.newpath = None 140 self.files = [] 141 self.newfiles = [] 142 143 self.retcode = None 144 self.renretcode = None 145 self.renfile = () 146 147 self.titles = [self._tr("Name"), self._tr("Size"), self._tr("Type"), 148 self._tr("Last Changed")] 149 150 PluginHost.registerCallbackProxy(self)
151
152 - def __del__(self):
154 155 @property
156 - def path(self):
157 return self._path
158 159 @path.setter
160 - def path(self, val):
161 self._reload(val)
162 163 @property
164 - def currentFiles(self):
165 return self.files
166
167 - def _reload(self, path=None):
168 if path: 169 self.newpath = path 170 else: 171 self.newpath = self._path 172 173 self.retcode = ts3lib.createReturnCode() 174 err = ts3lib.requestFileList(self.schid, self.cid, self.password, 175 self.newpath, self.retcode) 176 if err != ERROR_ok: 177 _errprint(self._tr("Error requesting filelist"), err, self.schid, 178 self.cid)
179
180 - def onFileListEvent(self, schid, channelID, path, name, size, date, 181 atype, incompletesize, returnCode):
182 if (schid != self.schid or channelID != self.cid or 183 returnCode != self.retcode): 184 return 185 186 self.newfiles.append(File(path, name, size, date, atype, 187 incompletesize))
188
189 - def onFileListFinishedEvent(self, schid, channelID, path):
190 if (schid != self.schid or channelID != self.cid or 191 path != self.newpath): 192 return
193 # might be unneeded (event is catched in onServerErrorEvent) 194
195 - def onServerErrorEvent(self, schid, errorMessage, error, returnCode, 196 extraMessage):
197 if schid != self.schid: 198 return 199 200 if returnCode == self.retcode: 201 if error in [ERROR_ok, ERROR_database_empty_result]: 202 self.beginResetModel() 203 self.files = self.newfiles 204 self.newfiles = [] 205 self.endResetModel() 206 207 if self._path != self.newpath: 208 self._path = self.newpath 209 self.pathChanged.emit(self._path) 210 else: 211 self.error.emit(self._tr("Error requesting filelist"), error, 212 errorMessage) 213 elif returnCode == self.renretcode: 214 if error != ERROR_ok: 215 self.renfile[0].name = self.renfile[1] 216 217 self.error.emit(self._tr("Error renaming file"), error, 218 errorMessage) 219 220 self.renfile = ()
221
222 - def onServerPermissionErrorEvent(self, schid, errorMessage, error, 223 returnCode, failedPermissionID):
224 if schid != self.schid or returnCode != self.retcode: 225 return 226 227 if returnCode == self.retcode: 228 if error != ERROR_ok: 229 self.error.emit(self._tr("Error requesting filelist"), error, 230 errorMessage) 231 elif returnCode == self.renretcode: 232 if error != ERROR_ok: 233 self.renfile[0].name = self.renfile[1] 234 235 self.error.emit(self._tr("Error renaming file"), error, 236 errorMessage) 237 238 self.renfile = ()
239
240 - def headerData(self, section, orientation, role=Qt.DisplayRole):
241 if role == Qt.DisplayRole and orientation == Qt.Horizontal: 242 return self.titles[section] 243 244 return None
245
246 - def flags(self, idx):
247 f = Qt.ItemIsEnabled | Qt.ItemIsSelectable 248 249 if not self.readonly: 250 return f | Qt.ItemIsEditable 251 else: 252 return f
253
254 - def index(self, row, column, parent=QModelIndex()):
255 if parent.isValid(): 256 return QModelIndex() 257 258 return self.createIndex(row, column)
259
260 - def parent(self, idx):
261 return QModelIndex()
262
263 - def rowCount(self, parent=QModelIndex()):
264 if parent.isValid(): 265 return 0 266 267 return len(self.files)
268
269 - def columnCount(self, parent=QModelIndex()):
270 return len(self.titles)
271
272 - def data(self, idx, role=Qt.DisplayRole):
273 if not idx.isValid(): 274 return None 275 276 f = self.files[idx.row()] 277 if idx.column() == 0: 278 if role == Qt.DisplayRole: 279 return f.name 280 elif role == Qt.DecorationRole: 281 return f.icon 282 elif role == Qt.EditRole and not self.readonly: 283 return f.name 284 elif role == Qt.UserRole: 285 if f.isDirectory: 286 return "a%s" % f.name 287 else: 288 return "b%s" % f.name 289 elif role == Qt.DisplayRole: 290 if idx.column() == 1 and not f.isDirectory: 291 return bytesToStr(f.size) 292 elif idx.column() == 2: 293 if f.isDirectory: 294 return self._tr("Directory") 295 else: 296 return self._tr("File") 297 elif idx.column() == 3: 298 return f.datetime.strftime(pytson.tr("filetransfer", 299 "%Y-%m-%d %H:%M:%S")) 300 return None
301
302 - def setData(self, idx, value, role=Qt.EditRole):
303 if not idx.isValid(): 304 return False 305 306 f = self.fileByIndex(idx) 307 if value == f.name: 308 return 309 310 self.renretcode = ts3lib.createReturnCode() 311 self.renfile = (f, f.name) 312 313 err = ts3lib.requestRenameFile(self.schid, self.cid, self.password, 314 0, "", f.fullpath, joinpath(f.path, 315 value), 316 self.renretcode) 317 318 if err == ERROR_ok: 319 f.name = value 320 return True
321
322 - def fileByIndex(self, idx):
323 if idx.isValid(): 324 return self.files[idx.row()] 325 return None
326
327 328 -class SmartStatusBar(QStatusBar):
329 """ 330 StatusBar which automatically hides itsself, when the message is cleared. 331 """ 332 333 # default Timeout of messages 334 defaultTimeout = 5000 335
336 - def __init__(self, parent=None):
337 super(SmartStatusBar, self).__init__(parent) 338 339 self.connect("messageChanged(QString)", self._onMessageChanged)
340
341 - def _onMessageChanged(self, message):
342 if message == "": 343 self.hide()
344
345 - def showMessage(self, message, timeout=0):
346 """ 347 Displays a message for a specified duration. 348 @param message: the message to display 349 @type message: str 350 @param timeout: duration in ms; optional; if set to 0, 351 SmartStatusBar.defaultTimeout is used 352 @type timeout: int 353 """ 354 self.show() 355 if timeout == 0: 356 timeout = self.defaultTimeout 357 358 QStatusBar.showMessage(self, message, timeout)
359
360 361 -class FileCollector(pytson.Translatable):
362 """ 363 Collects all files recursively from TS3 filetransfer directories with their 364 corresponding download path. 365 Emits a signal collectionFinished with a list of tuples(str, list[File]) 366 containing the download dir and a list of files. 367 The signal collectionError(str, int) is emitted on error with the 368 errorstring and the errorcode. 369 """ 370
371 - def __init__(self, schid, cid, password, rootdir):
372 """ 373 Instantiates a new object. 374 @param schid: the id of the serverconnection handler 375 @type schid: int 376 @param cid: the id of the channel 377 @type cid: int 378 @param password: the password of the channel 379 @type password: str 380 @param rootdir: the root download directory 381 @type rootdir: str 382 """ 383 super().__init__() 384 385 self.schid = schid 386 self.cid = cid 387 self.password = password 388 self.rootdir = rootdir 389 390 self.collectionFinished = Signal() 391 self.collectionError = Signal() 392 393 self.queue = {} 394 self.files = {} 395 396 PluginHost.registerCallbackProxy(self)
397
398 - def __del__(self):
400
401 - def addFiles(self, files):
402 """ 403 Manually adds a list of files to the collection (emitted with the 404 rootdir) 405 @param files: list of files to emit 406 @type files: list(File) 407 """ 408 self.files[self.rootdir] = files
409
410 - def collect(self, dirs):
411 """ 412 Starts collecting files from a list of directories 413 @param dirs: list of directories 414 @type dirs: list(File) 415 """ 416 for d in dirs: 417 retcode = ts3lib.createReturnCode() 418 self.queue[retcode] = d.fullpath 419 420 err = ts3lib.requestFileList(self.schid, self.cid, self.password, 421 d.fullpath, retcode) 422 423 if err != ERROR_ok: 424 del self.queue[retcode] 425 self.collectionError.emit(self._tr("Error requesting " 426 "filelist of {dirname}"). 427 format(dirname=d.fullpath), err)
428
429 - def onServerErrorEvent(self, schid, errorMessage, error, returnCode, 430 extraMessage):
431 if schid != self.schid or returnCode not in self.queue: 432 return 433 434 if error not in [ERROR_ok, ERROR_database_empty_result]: 435 self.collectionError.emit(self._tr("Error requesting filelist " 436 "of {dirname}"). 437 format(dirname=self.queue[returnCode]), 438 error) 439 440 del self.queue[returnCode] 441 442 if not self.queue: 443 self.collectionFinished.emit([(k, v) 444 for k, v in self.files.items()]) 445 self.files = {}
446
447 - def onFileListEvent(self, schid, channelID, path, name, size, datetime, 448 atype, incompletesize, returnCode):
449 if (schid != self.schid or self.cid != channelID or 450 returnCode not in self.queue): 451 return 452 453 downpath = os.path.join(self.rootdir, *splitpath(path)[1:]) 454 f = File(path, name, size, datetime, atype, incompletesize) 455 456 if f.isDirectory: 457 self.collect([f]) 458 else: 459 if downpath in self.files: 460 self.files[downpath].append(f) 461 else: 462 self.files[downpath] = [f]
463
464 465 -class FileBrowser(QDialog, pytson.Translatable):
466 """ 467 Dialog to display files contained on a TS3 filepath. 468 """ 469
470 - def __init__(self, schid, cid, password='', path='/', parent=None, 471 staticpath=False, readonly=False, downloaddir=None, 472 iconpack=None):
473 """ 474 Instantiates a new object. 475 @param schid: the id of the serverconnection handler 476 @type schid: int 477 @param cid: the id of the channel 478 @type cid: int 479 @param password: password to the channel, defaults to an empty string 480 @type password: str 481 @param path: path to display, defaults to the root path 482 @type path: str 483 @param parent: parent of the dialog; optional keyword arg; 484 defaults to None 485 @type parent: QWidget 486 @param staticpath: if set to True, the initial path can't be 487 changed by the user; optional keyword arg; defaults to False 488 @type staticpath: bool 489 @param readonly: if set to True, the user can't download, upload 490 or delete files, or create new directories; optional keyword arg; 491 defaults to False 492 @type readonly: bool 493 @param downloaddir: directory to download files to; optional keyword 494 arg; defaults to None; if set to None, the TS3 client's download 495 directory is used 496 @type downloaddir: str 497 @param iconpack: iconpack to load icons from; optional keyword arg; 498 defaults to None; if set to None, the current iconpack is used 499 @type iconpack: ts3client.IconPack 500 """ 501 super(QDialog, self).__init__(parent) 502 self.setAttribute(Qt.WA_DeleteOnClose) 503 504 iconpackopened = False 505 if not iconpack: 506 try: 507 iconpack = ts3client.IconPack.current() 508 iconpack.open() 509 iconpackopened = True 510 except Exception as e: 511 self.delete() 512 raise e 513 514 try: 515 setupUi(self, pytson.getPluginPath("ressources", "filebrowser.ui"), 516 iconpack=iconpack) 517 518 self.statusbar = SmartStatusBar(self) 519 self.layout().addWidget(self.statusbar) 520 self.statusbar.hide() 521 except Exception as e: 522 self.delete() 523 raise e 524 525 err, cname = ts3lib.getChannelVariableAsString(schid, cid, 526 ChannelProperties. 527 CHANNEL_NAME) 528 529 if err == ERROR_ok: 530 self.setWindowTitle(self._tr("File Browser - {cname}").format( 531 cname=cname)) 532 else: 533 self.setWindowTitle(self._tr("File Browser")) 534 535 self.schid = schid 536 self.cid = cid 537 self.password = password 538 self.path = None 539 540 self.staticpath = staticpath 541 self.readonly = readonly 542 543 self.createretcode = None 544 self.delretcode = None 545 546 if not self.readonly and not downloaddir: 547 cfg = ts3client.Config() 548 q = cfg.query("SELECT value FROM filetransfer " 549 "WHERE key='DownloadDir'") 550 del cfg 551 552 if q.next(): 553 self.downloaddir = q.value("value") 554 else: 555 self.delete() 556 raise Exception("Error getting DownloadDir from config") 557 else: 558 self.downloaddir = downloaddir 559 560 if not self.readonly: 561 menu = self.menu = QMenu(self) 562 563 self.openAction = menu.addAction(QIcon(iconpack.icon("FILE_UP")), 564 self._tr("Open")) 565 self.openAction.connect("triggered()", 566 self.on_openAction_triggered) 567 568 self.downAction = menu.addAction(QIcon(iconpack.icon("DOWN")), 569 self._tr("Download")) 570 self.downAction.connect("triggered()", self.downloadFiles) 571 self.renameAction = menu.addAction(QIcon(iconpack.icon("EDIT")), 572 self._tr("Rename")) 573 self.renameAction.connect("triggered()", 574 self.on_renameAction_triggered) 575 self.copyAction = menu.addAction(QIcon(iconpack.icon("COPY")), 576 self._tr("Copy URL")) 577 self.copyAction.connect("triggered()", 578 self.on_copyAction_triggered) 579 self.delAction = menu.addAction(QIcon(iconpack.icon("DELETE")), 580 self._tr("Delete")) 581 self.delAction.connect("triggered()", self.deleteFiles) 582 583 self.upAction = menu.addAction(QIcon(iconpack.icon("UP")), 584 self._tr("Upload files")) 585 self.upAction.connect("triggered()", self.uploadFiles) 586 self.createAction = menu.addAction(QIcon.fromTheme("folder"), 587 self._tr("Create Folder")) 588 self.createAction.connect("triggered()", self.createFolder) 589 self.refreshAction = menu.addAction(QIcon(iconpack.icon( 590 "FILE_REFRESH")), 591 self._tr("Refresh")) 592 self.refreshAction.connect("triggered()", self.refresh) 593 594 self.allactions = [self.openAction, self.downAction, 595 self.renameAction, self.copyAction, 596 self.delAction, self.upAction, 597 self.createAction, self.refreshAction] 598 599 self.collector = FileCollector(schid, cid, password, self.downloaddir) 600 self.collector.collectionFinished.connect(self._startDownload) 601 self.collector.collectionError.connect(self.showError) 602 603 self.fileDoubleClicked = Signal() 604 self.contextMenuRequested = Signal() 605 606 self.transdlg = None 607 608 self.listmodel = FileListModel(schid, cid, password, self, 609 readonly=readonly) 610 self.listmodel.pathChanged.connect(self.onPathChanged) 611 self.listmodel.error.connect(self.showError) 612 613 self.proxy = QSortFilterProxyModel(self) 614 self.proxy.setSortRole(Qt.UserRole) 615 self.proxy.setSortCaseSensitivity(Qt.CaseInsensitive) 616 self.proxy.setFilterCaseSensitivity(Qt.CaseInsensitive) 617 self.proxy.setSourceModel(self.listmodel) 618 619 self.listmodel.path = path 620 621 self._adjustUi() 622 623 if iconpackopened: 624 iconpack.close() 625 626 PluginHost.registerCallbackProxy(self)
627
628 - def __del__(self):
630
631 - def _enableMenus(self, actlist):
632 for act in self.allactions: 633 act.setVisible(act in actlist)
634
635 - def _adjustMenu(self):
636 selfiles = self.selectedFiles() 637 cur = self.listmodel.fileByIndex(self.currentItem()) 638 639 if len(selfiles) == 0: 640 self._enableMenus([self.upAction, self.createAction, 641 self.refreshAction]) 642 elif cur.isDirectory: 643 self._enableMenus([self.openAction, self.downAction, 644 self.renameAction, self.copyAction, 645 self.delAction]) 646 else: 647 self._enableMenus([self.downAction, self.renameAction, 648 self.copyAction, self.delAction])
649
650 - def _adjustUi(self):
651 self.filterFrame.hide() 652 653 self.filecountLabel.hide() 654 655 self.downloaddirButton.setText(self.downloaddir) 656 657 self.iconButton.setChecked(True) 658 self.stack.setCurrentWidget(self.listPage) 659 660 self.list.setModel(self.proxy) 661 self.table.setModel(self.proxy) 662 self.table.sortByColumn(0, Qt.AscendingOrder) 663 664 if self.staticpath: 665 self.upButton.hide() 666 self.homeButton.hide() 667 668 if self.readonly: 669 self.uploadButton.hide() 670 self.downloadButton.hide() 671 self.directoryButton.hide() 672 self.deleteButton.hide() 673 674 self.downloaddirLabel.hide() 675 self.downloaddirButton.hide() 676 677 header = self.table.horizontalHeader() 678 header.setSectionResizeMode(0, QHeaderView.Stretch) 679 for i in range(1, header.count()): 680 header.setSectionResizeMode(i, QHeaderView.ResizeToContents) 681 682 self.refreshButton.connect("clicked()", self.refresh) 683 self.uploadButton.connect("clicked()", self.uploadFiles) 684 self.downloadButton.connect("clicked()", self.downloadFiles) 685 self.deleteButton.connect("clicked()", self.deleteFiles) 686 self.directoryButton.connect("clicked()", self.createFolder) 687 self.list.connect("doubleClicked(QModelIndex)", self.viewDoubleClicked) 688 self.table.connect("doubleClicked(QModelIndex)", 689 self.viewDoubleClicked)
690
691 - def _showTransfers(self):
692 if not self.transdlg: 693 self.transdlg = FileTransferDialog(self.schid, self.cid, 694 self.password, self) 695 self.transdlg.show()
696
697 - def onPathChanged(self, newpath):
698 self.path = newpath 699 self.pathEdit.setText(newpath) 700 701 inroot = newpath == "/" 702 self.upButton.setEnabled(not inroot) 703 self.homeButton.setEnabled(not inroot) 704 705 files = self.listmodel.currentFiles 706 707 if not files: 708 self.filecountLabel.hide() 709 else: 710 self.filecountLabel.show() 711 712 fcount = 0 713 dcount = 0 714 715 for f in files: 716 if f.isDirectory: 717 dcount += 1 718 else: 719 fcount += 1 720 721 fstr = self._tr("{filecount} file(s)", n=fcount).format( 722 filecount=fcount) 723 dstr = self._tr("{dircount} directory(s)", n=dcount).format( 724 dircount=dcount) 725 726 if dcount == 0: 727 self.filecountLabel.setText(fstr) 728 elif fcount == 0: 729 self.filecountLabel.setText(dstr) 730 else: 731 cstr = self._tr("{dircountstr} and {fcountstr}").format( 732 dircountstr=dstr, fcountstr=fstr) 733 self.filecountLabel.setText(cstr)
734
735 - def on_pathEdit_returnPressed(self):
736 oldpath = self.listmodel.path 737 if not self.readonly: 738 self.listmodel.path = self.pathEdit.text 739 740 self.pathEdit.text = oldpath or ""
741
742 - def on_iconButton_toggled(self, act):
743 if act: 744 self.stack.setCurrentWidget(self.listPage)
745
746 - def on_detailedButton_toggled(self, act):
747 if act: 748 self.stack.setCurrentWidget(self.tablePage)
749
750 - def on_filterButton_clicked(self):
751 self.filterFrame.show()
752
753 - def on_clearButton_clicked(self):
754 self.filterEdit.clear() 755 self.filterFrame.hide()
756
757 - def on_filterEdit_textChanged(self, newtext):
758 self.proxy.setFilterRegExp(newtext)
759
760 - def on_upButton_clicked(self):
761 if self.staticpath: 762 return 763 if self.path == "/": 764 return 765 766 self.listmodel.path = joinpath(*splitpath(self.path)[:-1])
767
768 - def on_homeButton_clicked(self):
769 if self.staticpath: 770 return 771 772 self.listmodel.path = "/"
773
774 - def refresh(self):
775 self.listmodel.path = self.listmodel.path
776
778 QDesktopServices.openUrl(QUrl(self.downloaddir))
779
780 - def showError(self, prefix, errcode, msg=None):
781 if not msg: 782 err, msg = ts3lib.getErrorMessage(errcode) 783 else: 784 err = ERROR_ok 785 786 if err != ERROR_ok: 787 self.statusbar.showMessage("%s: %s" % (prefix, errcode)) 788 else: 789 self.statusbar.showMessage("%s: %s" % (prefix, msg))
790
791 - def uploadFiles(self):
792 if self.readonly: 793 return 794 795 files = QFileDialog.getOpenFileNames(self, self._tr("Upload files"), 796 self.downloaddir) 797 798 fca = FileCollisionAction.overwrite 799 curfiles = {f.name: f for f in self.listmodel.currentFiles} 800 for f in files: 801 fname = os.path.split(f)[-1] 802 if fname in curfiles: 803 if not fca & FileCollisionAction.toall: 804 fca = FileCollisionDialog.getAction(f, curfiles[fname], 805 False, len(files) > 1, 806 self) 807 808 if fca == 0: 809 return 810 811 if fca & FileCollisionAction.skip: 812 if not fca & FileCollisionAction.toall: 813 fca = FileCollisionAction.overwrite 814 break 815 816 self._showTransfers() 817 self.transdlg.addUpload(self.path, f, 818 fca & FileCollisionAction.overwrite, 819 fca & FileCollisionAction.resume) 820 821 if not fca & FileCollisionAction.toall: 822 fca = FileCollisionAction.overwrite
823
824 - def onServerErrorEvent(self, schid, errorMessage, error, returnCode, 825 extraMessage):
826 if schid != self.schid: 827 return 828 829 if returnCode == self.createretcode: 830 if error == ERROR_ok: 831 self.listmodel.path = self.path 832 else: 833 self.showError(self._tr("Error creating directory"), error, 834 errorMessage) 835 elif returnCode == self.delretcode: 836 if error == ERROR_ok: 837 self.listmodel.path = self.path 838 else: 839 self.showError(self._tr("Error deleting files"), error, 840 errorMessage)
841
842 - def selectedFiles(self):
843 if self.stack.currentWidget() == self.listPage: 844 view = self.list 845 else: 846 view = self.table 847 848 return [self.listmodel.fileByIndex(self.proxy.mapToSource(x)) 849 for x in view.selectionModel().selectedIndexes]
850
851 - def currentItem(self, source=True):
852 if self.stack.currentWidget() == self.listPage: 853 view = self.list 854 else: 855 view = self.table 856 857 if source: 858 return self.proxy.mapToSource(view.currentIndex()) 859 else: 860 return view.currentIndex()
861
862 - def _startDownload(self, collection):
863 """ 864 @param collection: list of tuples containing the download directory and 865 the list of files to download to that directory 866 @type collection: list[tuple(str, list[File])] 867 """ 868 if not collection: 869 return 870 871 fca = FileCollisionAction.overwrite 872 873 for (downdir, files) in collection: 874 for f in files: 875 multi = len(files) + len(collection) > 2 876 fname = os.path.join(downdir, f.name) 877 if os.path.isfile(fname): 878 if not fca & FileCollisionAction.toall: 879 fca = FileCollisionDialog.getAction(fname, f, True, 880 multi, self) 881 882 if fca == 0: 883 return 884 885 if fca & FileCollisionAction.skip: 886 if not fca & FileCollisionAction.toall: 887 fca = FileCollisionAction.overwrite 888 break 889 890 self._showTransfers() 891 self.transdlg.addDownload(f, downdir, 892 fca & FileCollisionAction.overwrite, 893 fca & FileCollisionAction.resume) 894 895 if not fca & FileCollisionAction.toall: 896 fca = FileCollisionAction.overwrite
897
898 - def downloadFiles(self, files=None):
899 if self.readonly: 900 return 901 902 if not files: 903 selfiles = self.selectedFiles() 904 else: 905 selfiles = files 906 907 if not selfiles: 908 return 909 910 downfiles = [] 911 downdirs = [] 912 913 for f in selfiles: 914 if f.isDirectory: 915 downdirs.append(f) 916 else: 917 downfiles.append(f) 918 919 if not downdirs: 920 self._startDownload([(self.downloaddir, downfiles)]) 921 else: 922 if downfiles: 923 self.collector.addFiles(downfiles) 924 925 self.collector.collect(downdirs)
926
927 - def createFolder(self):
928 if self.readonly: 929 return 930 931 ok = BoolResult() 932 dirname = QInputDialog.getText(self, self._tr("Create Folder"), 933 self._tr("Folder name:"), 934 QLineEdit.Normal, "", ok) 935 936 if not ok or dirname == "": 937 return 938 939 self.createretcode = ts3lib.createReturnCode() 940 err = ts3lib.requestCreateDirectory(self.schid, self.cid, 941 self.password, 942 joinpath(self.path, dirname), 943 self.createretcode) 944 945 if err != ERROR_ok: 946 self.showError(self._tr("Error creating directory"), err)
947
948 - def deleteFiles(self, files=None):
949 if self.readonly: 950 return 951 952 if not files: 953 selfiles = self.selectedFiles() 954 else: 955 selfiles = files 956 957 if not selfiles: 958 return 959 960 if QMessageBox.question(self, self._tr("Delete files"), 961 self._tr("Do you really want to delete all " 962 "selected files?")) == QMessageBox.No: 963 return 964 965 pathes = [f.fullpath for f in selfiles] 966 self.delretcode = ts3lib.createReturnCode() 967 err = ts3lib.requestDeleteFile(self.schid, self.cid, self.password, 968 pathes, self.delretcode) 969 970 if err != ERROR_ok: 971 self.showError(self._tr("Error deleting files"), err)
972
974 selfiles = self.selectedFiles() 975 globpos = self.table.mapToGlobal(pos) 976 977 if self.readonly: 978 self.contextMenuRequested.emit(selfiles, globpos) 979 else: 980 self._adjustMenu() 981 self.menu.popup(globpos)
982
984 selfiles = self.selectedFiles() 985 globpos = self.list.mapToGlobal(pos) 986 987 if self.readonly: 988 self.contextMenuRequested.emit(selfiles, globpos) 989 else: 990 self._adjustMenu() 991 self.menu.popup(globpos)
992
993 - def viewDoubleClicked(self, idx):
994 if not idx.isValid(): 995 return 996 997 f = self.listmodel.fileByIndex(self.proxy.mapToSource(idx)) 998 if f.isDirectory: 999 if self.staticpath: 1000 self.fileDoubleClicked.emit(f) 1001 else: 1002 self.listmodel.path = f.fullpath 1003 else: 1004 if self.readonly: 1005 self.fileDoubleClicked.emit(f) 1006 else: 1007 self.downloadFiles([f])
1008
1009 - def on_openAction_triggered(self):
1010 cur = self.listmodel.fileByIndex(self.currentItem()) 1011 1012 if not cur or not cur.isDirectory: 1013 return 1014 1015 self.listmodel.path = cur.fullpath
1016
1017 - def on_renameAction_triggered(self):
1018 if self.stack.currentWidget() == self.listPage: 1019 view = self.list 1020 else: 1021 view = self.table 1022 1023 view.edit(self.currentItem(False))
1024
1025 - def on_copyAction_triggered(self):
1026 cur = self.listmodel.fileByIndex(self.currentItem()) 1027 1028 if not cur: 1029 return 1030 1031 err, host, port, _ = ts3lib.getServerConnectInfo(self.schid) 1032 1033 if err == ERROR_ok: 1034 url = ("[URL=ts3file://{address}?port={port}&channel={cid}&" 1035 "path={path}&filename={fname}&isDir={isdir}&" 1036 "size={size}&fileDateTime={date}]{fname}[/URL]").format( 1037 address=host, port=port, cid=self.cid, 1038 path=QUrl.toPercentEncoding(cur.path), fname=cur.name, 1039 isdir=1 if cur.isDirectory else 0, size=cur.size, 1040 date=int(cur.datetime.timestamp())) 1041 1042 QApplication.clipboard().setText(url) 1043 else: 1044 self.showError(self._tr("Error getting server connection info"), 1045 err)
1046
1047 1048 -class FileCollisionAction(object):
1049 overwrite = 1 1050 resume = 2 1051 skip = 4 1052 toall = 8
1053
1054 1055 -class FileCollisionDialog(QDialog, pytson.Translatable):
1056 """ 1057 Dialog to inform about a filecollision and requests input how to handle it. 1058 """ 1059 1060 @classmethod
1061 - def getAction(cls, localfile, remotefile, isdownload, multi, parent=None):
1062 """ 1063 Convenience function to execute (blocks) the dialog. 1064 @param localfile: the path to the local file 1065 @type localfile: str 1066 @param remotefile: the remote file 1067 @type remotefile: File 1068 @param isdownload: set to True if remotefile should be downloaded 1069 @type isdownload: bool 1070 @param multi: set to True, if there are multiple files which could 1071 collide 1072 @type multi: bool 1073 @param parent: parent widget of the dialog; optional; defaults to None 1074 @type parent: QWidget 1075 """ 1076 dlg = cls(localfile, remotefile, isdownload, multi, parent) 1077 return dlg.exec_()
1078
1079 - def __init__(self, localfile, remotefile, isdownload, multi, parent=None):
1080 """ 1081 Instantiates a new dialog. 1082 @param localfile: the path to the local file 1083 @type localfile: str 1084 @param remotefile: the remote file 1085 @type remotefile: File 1086 @param isdownload: set to True if remotefile should be downloaded 1087 @type isdownload: bool 1088 @param multi: set to True, if there are multiple files which could 1089 collide 1090 @type multi: bool 1091 @param parent: parent widget of the dialog; optional; defaults to None 1092 @type parent: QWidget 1093 """ 1094 super(QDialog, self).__init__(parent) 1095 self.setAttribute(Qt.WA_DeleteOnClose) 1096 1097 try: 1098 setupUi(self, pytson.getPluginPath("ressources", 1099 "filecollision.ui")) 1100 except Exception as e: 1101 self.delete() 1102 raise e 1103 1104 if not multi: 1105 self.multiButton.setVisible(False) 1106 self.skipallButton.setVisible(False) 1107 1108 locsize = os.path.getsize(localfile) 1109 if locsize == remotefile.size: 1110 self.resumeButton.hide() 1111 1112 self.actionLabel.text = self._tr("Do you want to overwrite the " 1113 "existing file") 1114 else: 1115 self.actionLabel.text = self._tr("Do you want to overwrite or " 1116 "resume the existing file") 1117 1118 self.filenameLabel.text = "<b>%s</b>" % remotefile.name 1119 1120 datefmt = pytson.tr("filetransfer", "%Y-%m-%d %H:%M:%S") 1121 locdate = datetime.fromtimestamp(os.path.getmtime(localfile)) 1122 filefmt = "Size: <b>%s</b><br />Date: <b>%s</b>" 1123 1124 locstr = filefmt % (bytesToStr(locsize), locdate.strftime(datefmt)) 1125 remstr = filefmt % (bytesToStr(remotefile.size), 1126 remotefile.datetime.strftime(datefmt)) 1127 1128 if isdownload: 1129 self.existingLabel.text = locstr 1130 self.newLabel.text = remstr 1131 else: 1132 self.existingLabel.text = remstr 1133 self.newLabel.text = locstr 1134 1135 self.adjustSize()
1136
1137 - def on_overwriteButton_clicked(self):
1138 ret = FileCollisionAction.overwrite 1139 if self.multiButton.isChecked(): 1140 ret = ret | FileCollisionAction.toall 1141 1142 self.done(ret)
1143
1144 - def on_resumeButton_clicked(self):
1145 ret = FileCollisionAction.resume 1146 if self.multiButton.isChecked(): 1147 ret = ret | FileCollisionAction.toall 1148 1149 self.done(ret)
1150
1151 - def on_skipButton_clicked(self):
1152 ret = FileCollisionAction.skip 1153 if self.multiButton.isChecked(): 1154 ret = ret | FileCollisionAction.toall 1155 1156 self.done(ret)
1157
1158 - def on_skipallButton_clicked(self):
1160
1161 - def on_cancelButton_clicked(self):
1162 self.done(0)
1163
1164 1165 -class FileTransfer(pytson.Translatable):
1166 """ 1167 Abstract container class to hold information on a filetransfer 1168 """ 1169
1170 - def __init__(self, err, retcode):
1171 self.err = err 1172 self.retcode = retcode 1173 1174 if err != ERROR_ok: 1175 errerr, msg = ts3lib.getErrorMessage(err) 1176 if errerr == ERROR_ok: 1177 self.errmsg = msg 1178 else: 1179 self.errmsg = self._tr("Unknown error {errcode}").format( 1180 errcode=err) 1181 1182 self.size = 0
1183
1184 - def updateSize(self, val):
1185 self.size = val
1186
1187 - def updateError(self, err, msg=None):
1188 self.err = err 1189 if msg: 1190 self.errmsg = msg 1191 else: 1192 errerr, msg = ts3lib.getErrorMessage(err) 1193 if errerr == ERROR_ok: 1194 self.errmsg = msg 1195 else: 1196 self.errmsg = self._tr("Unknown error {errcode}").format( 1197 errcode=err)
1198 1199 @property
1200 - def progress(self):
1201 return 0
1202 1203 @property
1204 - def hasError(self):
1205 return self.err != ERROR_ok
1206
1207 1208 -class Download(FileTransfer, pytson.Translatable):
1209 """ 1210 Container class to hold information on a download 1211 """ 1212
1213 - def __init__(self, err, retcode, thefile, todir):
1214 super().__init__(err, retcode) 1215 1216 self.file = thefile 1217 self.todir = todir
1218 1219 @property
1220 - def progress(self):
1221 if self.size == -1: 1222 return 100 1223 return round((self.size / self.file.size) * 100)
1224 1225 @property
1226 - def description(self):
1227 if self.hasError: 1228 return self._tr("Error downloading {filename}: {errmsg}").format( 1229 filename=self.file.name, errmsg=self.errmsg) 1230 else: 1231 return self._tr("Downloading {filename}").format( 1232 filename=self.file.name)
1233 1234 @property
1235 - def localpath(self):
1236 return os.path.join(self.todir, self.file.name)
1237
1238 1239 -class Upload(FileTransfer, pytson.Translatable):
1240 """ 1241 Container class to hold information on an upload 1242 """ 1243
1244 - def __init__(self, err, retcode, localfile):
1245 super().__init__(err, retcode) 1246 1247 self.file = localfile 1248 self.completesize = os.path.getsize(localfile)
1249 1250 @property
1251 - def progress(self):
1252 if self.size == -1: 1253 return 100 1254 return round((self.size / self.completesize) * 100)
1255 1256 @property
1257 - def description(self):
1258 if self.hasError: 1259 return self._tr("Error uploading {filename}: {errmsg}").format( 1260 filename=self.file, errmsg=self.errmsg) 1261 else: 1262 return self._tr("Uploading {filename}").format(filename=self.file)
1263
1264 1265 -class FileTransferModel(QAbstractItemModel, pytson.Translatable):
1266 """ 1267 Itemmodel to abstract multiple filetransfers. 1268 """ 1269
1270 - def __init__(self, schid, cid, password, parent=None):
1271 super().__init__(parent) 1272 1273 self.schid = schid 1274 self.cid = cid 1275 self.password = password 1276 1277 self.titles = [self._tr("Description"), self._tr("Progress")] 1278 1279 self.transfers = OrderedDict() 1280 1281 self.downcounter = 0 1282 self.timer = None 1283 1284 PluginHost.registerCallbackProxy(self)
1285
1286 - def __del__(self):
1288
1289 - def timerEvent(self, event):
1290 for i, trans in enumerate(self.transfers.values()): 1291 if isinstance(trans, Download): 1292 if not trans.hasError and trans.progress != 100: 1293 trans.updateSize(os.path.getsize(trans.localpath)) 1294 idx = self.createIndex(i, 1) 1295 self.dataChanged(idx, idx)
1296
1297 - def addDownload(self, thefile, downloaddir, overwrite, resume):
1298 """ 1299 Requests a download from the server and monitors its progress 1300 @param thefile: remote file to download 1301 @type thefile: File 1302 @param downloaddir: path to the download directory 1303 @type downloaddir: str 1304 @param overwrite: set to True to overwrite an existing file 1305 @type overwrite: bool 1306 @param resume: set to True to resume a previous download 1307 @type resume: bool 1308 @return: the filetransfer id 1309 @rtype: int 1310 """ 1311 retcode = ts3lib.createReturnCode() 1312 err, ftid = ts3lib.requestFile(self.schid, self.cid, self.password, 1313 thefile.fullpath, overwrite, resume, 1314 downloaddir, retcode) 1315 1316 self.beginInsertRows(QModelIndex(), len(self.transfers), 1317 len(self.transfers)) 1318 self.transfers[ftid] = Download(err, retcode, thefile, downloaddir) 1319 self.endInsertRows() 1320 1321 self.downcounter += 1 1322 if self.downcounter == 1: 1323 self.timer = self.startTimer(500) 1324 1325 return ftid
1326
1327 - def addUpload(self, path, localfile, overwrite, resume):
1328 """ 1329 Requests an upload to the server. 1330 @param path: path to upload the file to 1331 @type path: str 1332 @param localfile: path to the file to upload 1333 @type localfile: str 1334 @param overwrite: set to True to overwrite an existing file 1335 @type overwrite: bool 1336 @param resume: set to True to resume a previous upload 1337 @type resume: bool 1338 @return: the filetransfer id 1339 @rtype: int 1340 """ 1341 retcode = ts3lib.createReturnCode() 1342 err, ftid = ts3lib.sendFile(self.schid, self.cid, self.password, 1343 joinpath(path, 1344 os.path.split(localfile)[-1]), 1345 overwrite, resume, 1346 os.path.dirname(localfile), retcode) 1347 1348 self.beginInsertRows(QModelIndex(), len(self.transfers), 1349 len(self.transfers)) 1350 self.transfers[ftid] = Upload(err, retcode, localfile) 1351 self.endInsertRows() 1352 1353 return ftid
1354
1355 - def cleanup(self):
1356 """ 1357 Cleanup finished and broken downloads 1358 """ 1359 for i, ftid in enumerate(list(self.transfers)): 1360 trans = self.transfers[ftid] 1361 if trans.progress == 100 or trans.hasError: 1362 self.beginRemoveRows(QModelIndex(), i, i) 1363 del self.transfers[ftid] 1364 self.endRemoveRows()
1365
1366 - def onFileTransferStatusEvent(self, transferID, status, statusMessage, 1367 remotefileSize, schid):
1368 if schid != self.schid or transferID not in self.transfers: 1369 return 1370 1371 i = list(self.transfers).index(transferID) 1372 idx = self.createIndex(i, 1) 1373 trans = self.transfers[transferID] 1374 if status == ERROR_file_transfer_complete: 1375 trans.updateSize(-1) 1376 1377 if isinstance(trans, Download): 1378 self.downcounter -= 1 1379 else: 1380 # sadly, there are no events during transmission, then the next 1381 # line would make sense 1382 self.transfers[transferID].updateSize(remotefileSize) 1383 1384 self.dataChanged(idx, idx) 1385 1386 if self.downcounter == 0 and self.timer: 1387 self.killTimer(self.timer)
1388
1389 - def onServerErrorEvent(self, schid, errorMessage, error, returnCode, 1390 extraMessage):
1391 if schid != self.schid or error != ERROR_ok: 1392 return 1393 1394 for i, trans in enumerate(self.transfers.values()): 1395 if trans.retcode == returnCode: 1396 trans.updateError(error, errorMessage) 1397 1398 idx = self.createIndex(i, 0) 1399 self.dataChanged(idx, idx) 1400 1401 return
1402
1403 - def onServerPermissionErrorEvent(self, *args):
1404 self.onServerErrorEvent(*args)
1405
1406 - def headerData(self, section, orientation, role=Qt.DisplayRole):
1407 if role == Qt.DisplayRole and orientation == Qt.Horizontal: 1408 return self.titles[section] 1409 1410 return None
1411
1412 - def index(self, row, column, parent=QModelIndex()):
1413 if parent.isValid(): 1414 return QModelIndex() 1415 1416 return self.createIndex(row, column)
1417
1418 - def parent(self, idx):
1419 return QModelIndex()
1420
1421 - def rowCount(self, parent=QModelIndex()):
1422 if parent.isValid(): 1423 return 0 1424 1425 return len(self.transfers)
1426
1427 - def columnCount(self, parent=QModelIndex()):
1428 return 2
1429
1430 - def data(self, idx, role=Qt.DisplayRole):
1431 if not idx.isValid(): 1432 return None 1433 1434 trans = list(self.transfers.values())[idx.row()] 1435 1436 if idx.column() == 0: 1437 if role == Qt.DisplayRole: 1438 return trans.description 1439 elif role == Qt.ForegroundRole: 1440 if trans.hasError: 1441 return Qt.red 1442 elif idx.column() == 1: 1443 if role == Qt.DisplayRole: 1444 return trans.progress 1445 1446 return None
1447
1448 1449 -class FileTransferDelegate(QStyledItemDelegate):
1450 """ 1451 Delegate which displays a progress bar in the second column of an itemview 1452 """
1453 - def paint(self, painter, option, idx):
1454 if idx.column() != 1: 1455 QStyledItemDelegate.paint(self, painter, option, idx) 1456 return 1457 1458 progress = idx.data() 1459 1460 pgoptions = QStyleOptionProgressBar() 1461 pgoptions.rect = option.rect 1462 pgoptions.minimum = 0 1463 pgoptions.maximum = 100 1464 pgoptions.progress = progress 1465 pgoptions.text = "%s%%" % progress 1466 pgoptions.textVisible = True 1467 1468 QApplication.style().drawControl(QStyle.CE_ProgressBar, pgoptions, 1469 painter)
1470
1471 1472 -class FileTransferDialog(QDialog, pytson.Translatable):
1473 """ 1474 Dialog to display filetransfers from/to a ts3 channel. 1475 """ 1476
1477 - def __init__(self, schid, cid, password, parent=None):
1478 super(QDialog, self).__init__(parent) 1479 self.setAttribute(Qt.WA_DeleteOnClose) 1480 1481 try: 1482 setupUi(self, pytson.getPluginPath("ressources", 1483 "filetransfer.ui")) 1484 1485 self.delegate = FileTransferDelegate(self) 1486 self.table.setItemDelegate(self.delegate) 1487 1488 self.model = FileTransferModel(schid, cid, password, self) 1489 self.table.setModel(self.model) 1490 except Exception as e: 1491 self.delete() 1492 raise e 1493 1494 self.resize(770, 250)
1495
1496 - def on_closeButton_clicked(self):
1497 self.close()
1498
1499 - def on_cleanupButton_clicked(self):
1500 self.model.cleanup()
1501
1502 - def addUpload(self, path, localfile, overwrite, resume):
1503 """ 1504 Adds an upload. 1505 @param path: path to upload the file to 1506 @type path: str 1507 @param localfile: path to the file to upload 1508 @type localfile: str 1509 @param overwrite: set to True to overwrite an existing file 1510 @type overwrite: bool 1511 @param resume: set to True to resume a previous upload 1512 @type resume: bool 1513 @return: the filetransfer id 1514 @rtype: int 1515 """ 1516 return self.model.addUpload(path, localfile, overwrite, resume)
1517
1518 - def addDownload(self, thefile, downloaddir, overwrite, resume):
1519 """ 1520 Adds a download. 1521 @param thefile: remote file to download 1522 @type thefile: File 1523 @param downloaddir: path to the download directory 1524 @type downloaddir: str 1525 @param overwrite: set to True to overwrite an existing file 1526 @type overwrite: bool 1527 @param resume: set to True to resume a previous download 1528 @type resume: bool 1529 @return: the filetransfer id 1530 @rtype: int 1531 """ 1532 return self.model.addDownload(thefile, downloaddir, overwrite, resume)
1533