#!/usr/bin/python3 from PyQt5.QtWidgets import ( QProgressDialog, QMainWindow, QDialog, QFileDialog, QApplication) from PyQt5.QtGui import ( QStandardItemModel,QStandardItem, QDesktopServices, QIcon, ) from PyQt5.QtCore import ( QLibraryInfo, QUrl, QItemSelectionModel, QFileInfo ) from PyQt5 import QtCore # , Qt, QThread, QObject, pyqtSignal) import sys try: from . import mageiaSyncUI from . import mageiaSyncExt from . import mageiaSyncDBprefs from . import mageiaSyncDBprefs0 from . import mageiaSyncDBrename from . import mageiaSyncAbout from . import mageiaSyncCredits except: import mageiaSyncUI import mageiaSyncExt import mageiaSyncDBprefs import mageiaSyncDBprefs0 import mageiaSyncDBrename import mageiaSyncAbout import mageiaSyncCredits class prefsDialog(QDialog,mageiaSyncDBprefs.Ui_prefsDialog ): def __init__(self, parent=None): QDialog.__init__(self,parent) self.setupUi(self) self.selectDest.clicked.connect(isosSync.selectDestination) class prefsDialog0(QDialog,mageiaSyncDBprefs0.Ui_prefsDialog0 ): def __init__(self, parent=None): QDialog.__init__(self,parent) self.setupUi(self) class renameDialog(QDialog,mageiaSyncDBrename.Ui_renameDialog ): # Display a dialog box to choose to rename an old collection of ISOs to a new one def __init__(self, parent=None): QDialog.__init__(self,parent) self.setupUi(self) self.chooseDir.clicked.connect(isosSync.renameDir) class aboutDialog(QDialog,mageiaSyncAbout.Ui_aboutDialog ): def __init__(self, parent=None): QDialog.__init__(self,parent) self.setupUi(self) self.creditsButton.clicked.connect(isosSync.credits) class creditsDialog(QDialog,mageiaSyncCredits.Ui_creditsDialog ): def __init__(self, parent=None): QDialog.__init__(self,parent) self.setupUi(self) text='' try: with open('/usr/share/doc/python-mageiasync/README.md', 'r') as f: block = f.readlines() for line in block: text+= line except: pass self.Readme.setText(text) class LogWindow(QProgressDialog): # Display a box at start during the remote directory list loading def __init__(self, parent=None): super(LogWindow, self).__init__(parent) self.setWindowModality(QtCore.Qt.WindowModal) self.setWindowTitle('Loading...') self.setLabelText(self.tr('Loading images list from repository.')) self.setMinimum(0) self.setMaximum(0) self.setAutoReset(False) self.setAutoClose(False) self.setMinimumDuration(1) def perform(self): self.progressDialog.setValue(self.progress) class dbWarning(QProgressDialog): # Display a box at start during the remote directory list loading def __init__(self, parent=None): super(dbWarning, self).__init__(parent) self.setWindowModality(QtCore.Qt.WindowModal) self.setWindowTitle(self.tr('Loading...')) self.setLabelText(self.tr('Loading images list from repository.')) self.setMinimum(0) self.setMaximum(0) self.setAutoReset(False) self.setAutoClose(False) self.setMinimumDuration(1) def perform(self): self.progressDialog.setValue(self.progress) class IsosViewer(QMainWindow, mageiaSyncUI.Ui_mainWindow): # Display the main window def __init__(self, parent=None): super(IsosViewer, self).__init__(parent) self.setupUi(self) self.connectActions() self.IprogressBar.setMinimum(0) self.IprogressBar.setMaximum(100) self.IprogressBar.setValue(0) self.IprogressBar.setEnabled(False) self.selectAllState=True self.stop.setEnabled(False) self.destination='' self.rsyncThread = mageiaSyncExt.syncThread(self) # create a thread to launch rsync self.rsyncThread.progressSignal.connect(self.setProgress) self.rsyncThread.speedSignal.connect(self.setSpeed) self.rsyncThread.sizeSignal.connect(self.setSize) self.rsyncThread.remainSignal.connect(self.setRemain) self.rsyncThread.endSignal.connect(self.syncEnd) self.rsyncThread.lvM.connect(self.lvMessage) self.rsyncThread.checkSignal.connect(self.checks) self.checkThreads=[] # A list of thread for each iso # Model for local list view in a table self.model = QStandardItemModel(0, 5, self) headers=[self.tr("Directory"),self.tr("Name"),self.tr("Size"),"SHA512","MD5"] i=0 for label in headers: self.model.setHeaderData(i, QtCore.Qt.Horizontal,label ) i+=1 # Model for remote list view in a table self.modelRemote = QStandardItemModel(0, 4, self) headers=[self.tr("Directory"),self.tr("Name"),self.tr("Size"),self.tr("Date")] i=0 for label in headers: self.modelRemote.setHeaderData(i, QtCore.Qt.Horizontal,label ) i+=1 # settings for the local list view self.localList.setModel(self.model) self.localList.setColumnWidth(0,230) self.localList.setColumnWidth(1,230) self.localList.setColumnWidth(2,140) self.localList.horizontalHeader().setStretchLastSection(True) self.localList.setSelectionMode(0) # NoSelection # settings for local iso names management self.localListNames=[] # settings for the remote list view self.listIsos.setModel(self.modelRemote) self.listIsos.setColumnWidth(0,160) self.listIsos.setColumnWidth(1,350) self.listIsos.setColumnWidth(2,120) self.listIsos.horizontalHeader().setStretchLastSection(True) self.listIsos.setSelectionMode(2) # MultiSelection self.listIsosNames=[] def add(self, path,iso,isoSize,date): # Add an remote ISO in list itemPath=QStandardItem(path) itemPath.setData(path,3) # Add tooltip itemIso=QStandardItem(iso) itemIso.setData(iso,3) # Add tooltip if isoSize==0: itemSize=QStandardItem('--') else: formatedSize=isoSize.replace(","," ") itemSize=QStandardItem(formatedSize) itemSize.setData(formatedSize,3) # Add tooltip itemSize.setTextAlignment(QtCore.Qt.AlignVCenter|QtCore.Qt.AlignHCenter) itemDate=QStandardItem(date) itemDate.setData(date,3) # Add tooltip itemDate.setTextAlignment(QtCore.Qt.AlignVCenter|QtCore.Qt.AlignHCenter) self.modelRemote.appendRow([itemPath,itemIso,itemSize,itemDate,]) self.listIsosNames.append([path,iso]) def localAdd(self, path,iso,isoSize): # Add an entry in local ISOs list, with indications about checking itemPath=QStandardItem(path) itemPath.setData(path,3) # Add tooltip itemIso=QStandardItem(iso) itemIso.setData(iso,3) # Add tooltip if isoSize==0: itemSize=QStandardItem('--') else: formatedSize='{:n}'.format(isoSize).replace(","," ") itemSize=QStandardItem(formatedSize) itemSize.setData(formatedSize,3) # Add tooltip itemSize.setTextAlignment(QtCore.Qt.AlignVCenter|QtCore.Qt.AlignHCenter) itemCheck1=QStandardItem("--") itemCheck1.setTextAlignment(QtCore.Qt.AlignVCenter|QtCore.Qt.AlignHCenter) itemCheck5=QStandardItem("--") itemCheck5.setTextAlignment(QtCore.Qt.AlignVCenter|QtCore.Qt.AlignHCenter) self.model.appendRow([itemPath,itemIso,itemSize,itemCheck1, itemCheck5,]) self.localListNames.append([path,iso]) def setProgress(self, value): # Update the progress bar self.IprogressBar.setValue(value) def setSpeed(self, value): # Update the speed field self.speedLCD.display(value) def setSize(self, size): # Update the size field self.Lsize.setText(size+self.tr(" bytes")) def setRemain(self,remainTime): content=QtCore.QTime.fromString(remainTime,"h:mm:ss") self.timeRemaining.setTime(content) def manualChecks(self): remoteRow=-1 for isoIndex in self.listIsos.selectionModel().selectedIndexes(): if remoteRow != isoIndex.row(): remoteRow = isoIndex.row() path = self.modelRemote.data(self.modelRemote.index(remoteRow,0)) name = self.modelRemote.data(self.modelRemote.index(remoteRow,1)) try: # Look for ISO in local list item=self.model.findItems(name,QtCore.Qt.MatchExactly,1)[0] except: # Remote ISO is not yet in local directory. We add it in localList and create the directory self.localAdd(path,name,0) basedir=QtCore.QDir(self.destination) basedir.mkdir(path) item=self.model.findItems(name,QtCore.Qt.MatchExactly,1)[0] row=self.model.indexFromItem(item).row() self.checks(row) def checks(self,isoIndex): # processes a checking for each iso # launches a thread for each iso newThread=mageiaSyncExt.checkThread(self) self.checkThreads.append(newThread) self.checkThreads[-1].setup(self.destination, self.model.data(self.model.index(isoIndex,0)) , self.model.data(self.model.index(isoIndex,1)), isoIndex) self.checkThreads[-1].md5Signal.connect(self.md5Check) self.checkThreads[-1].sha512Signal.connect(self.sha512Check) # self.checkThreads[-1].dateSignal.connect(self.dateCheck) self.checkThreads[-1].sizeFinalSignal.connect(self.sizeUpdate) self.checkThreads[-1].checkStartSignal.connect(self.checkStart) self.checkThreads[-1].start() def checkStart(self,isoIndex): # the function indicates that checking is in progress # the hundred contains index of the value to check, the minor value contains the row col=(int)(isoIndex/100) row=isoIndex-col*100 self.model.setData(self.model.index(row, col, QtCore.QModelIndex()), self.tr("Checking")) def md5Check(self,check): verified=False signed=True if check>=128: val=self.tr("OK") row=check-128 if row >= 64: verified=True row -= 64 if row >= 32: row -= 32 signed=False else: val=self.tr("Failed") row=check if row >= 64: verified=True row -= 64 if row >=32: signed=False row-=32 if not signed: print("Row: ", row) self.lvMessage("Signature for %s.md5 not found"%self.model.data(self.model.index(row,1))) if verified: # we add an icon for the GPG key self.model.setData(self.model.index(row, 4, QtCore.QModelIndex()),QIcon("preflight-verifier"),1) self.lvMessage("MD5 Signature OK") self.model.setData(self.model.index(row, 4, QtCore.QModelIndex()), val) def sha512Check(self,check): verified=False signed=True if check>=128: val=self.tr("OK") row=check-128 if row >= 64: verified=True row -= 64 if row >= 32: row -= 32 signed=False else: val=self.tr("Failed") row=check if row >= 64: verified=True row -= 64 if row >=32: signed=False row-=32 print(row) if not signed: self.lvMessage("Signature for %s.sha512 not found"%self.model.data(self.model.index(row,1))) if verified: # we add an icon for the GPG key self.lvMessage("Sha512 signature OK") self.model.setData(self.model.index(row, 3, QtCore.QModelIndex()),QIcon("preflight-verifier"),1) self.model.setData(self.model.index(row, 3, QtCore.QModelIndex()), val) def sizeUpdate(self,signal,isoSize): col=(int)(signal/100) row=signal-col*100 self.model.setData(self.model.index(row, col, QtCore.QModelIndex()), isoSize) def syncEnd(self, rc): if rc==1: self.lvMessage(self.tr("Command rsync not found")) elif rc==2: self.lvMessage(self.tr("Error in rsync parameters")) elif rc==3: self.lvMessage(self.tr("Unknown error in rsync")) self.IprogressBar.setEnabled(False) self.syncGo.setEnabled(True) self.listIsos.setEnabled(True) self.selectAll.setEnabled(True) self.stop.setEnabled(False) def prefsInit(self): # Load the parameters at first params=QtCore.QSettings("Mageia","mageiaSync") paramRelease="" try: paramRelease=params.value("release", type="QString") # the parameters already initialised? except: pass if paramRelease =="": # Values are not yet set self.pd0=prefsDialog0() self.pd0.user.setFocus() answer=self.pd0.exec_() if answer: # Update params self.user=self.pd0.user.text() self.password=self.pd0.password.text() self.location=self.pd0.location.text() params=QtCore.QSettings("Mageia","mageiaSync") params.setValue("user",self.user) params.setValue("password",self.password) params.setValue("location",self.location) else: pass # answer=QDialogButtonBox(QDialogButtonBox.Ok) # the user must set values or default values self.pd0.close() self.pd=prefsDialog() if self.password !="": code,list=mageiaSyncExt.findRelease('rsync://'+self.user+'@bcd.mageia.org/isos/',self.password) if code==0: for item in list: self.pd.release.addItem(item) self.pd.password.setText(self.password) self.pd.user.setText(self.user) self.pd.location.setText(self.location) self.pd.selectDest.setText(QtCore.QDir.currentPath()) self.pd.release.setFocus() answer=self.pd.exec_() if answer: # Update params self.user=self.pd.user.text() self.password=self.pd.password.text() self.location=self.pd.location.text() params=QtCore.QSettings("Mageia","mageiaSync") self.release= self.pd.release.currentText() self.destination=self.pd.selectDest.text() self.bwl=self.pd.bwl.value() params.setValue("release", self.release) params.setValue("user",self.user) params.setValue("password",self.password) params.setValue("location",self.location) params.setValue("destination",self.destination) params.setValue("bwl",str(self.bwl)) else: pass print(self.tr("the user must set values or default values")) self.pd.close() else: self.release=params.value("release", type="QString") self.user=params.value("user", type="QString") self.location=params.value("location", type="QString") self.password=params.value("password", type="QString") self.destination=params.value("destination", type="QString") self.bwl=params.value("bwl",type=int) dest = QFileInfo(self.destination) if dest.exists() and dest.isDir(): self.localDirLabel.setText(self.tr("Local directory: ")+self.destination) else: #; {} is the placeholder the directory anme self.localDirLabel.setText(self.tr("/!\ Local directory {} doesn't exists or isn't accessible. Check mounts or settings.").format(self.destination)) if self.location !="": self.remoteDirLabel.setText(self.tr("Remote directory: ")+self.location) def selectDestination(self): # dialog box to select the destination (local directory) directory = QFileDialog.getExistingDirectory(self, self.tr('Select a directory'),self.destination) if directory != "": self.pd.selectDest.setText(directory) def selectAllIsos(self): # Select or unselect the ISOs in remote list if self.selectAllState : selectMode = QItemSelectionModel.Select self.selectAll.setText(self.tr("Unselect &All")) else: selectMode = QItemSelectionModel.Deselect self.selectAll.setText(self.tr("Select &All")) for i in range(self.modelRemote.rowCount()): self.listIsos.selectionModel().select(self.modelRemote.index(i,0),QItemSelectionModel.Rows| selectMode) # set flag to selected or deselected self.selectAllState=not self.selectAllState def connectActions(self): self.actionQuit.triggered.connect(app.quit) self.quit.clicked.connect(app.quit) self.actionRename.triggered.connect(self.rename) self.actionUpdate.triggered.connect(self.updateList) self.actionCheck.triggered.connect(self.manualChecks) self.actionPreferences.triggered.connect(self.prefs) self.syncGo.clicked.connect(self.launchSync) self.selectAll.clicked.connect(self.selectAllIsos) self.actionAbout.triggered.connect(self.about) self.actionOnline_help.triggered.connect(self.help) def updateList(self): # From the menu entry self.lw = LogWindow() self.lw.show() self.modelRemote.removeRows(0,self.modelRemote.rowCount()) self.model.removeRows(0,self.model.rowCount()) if self.location == "" : self.nameWithPath='rsync://'+self.user+'@bcd.mageia.org/isos/'+self.release+'/' # print self.nameWithPath else: self.nameWithPath=self.location+'/' self.lvMessage(self.tr("Source: ")+self.nameWithPath) self.fillList = mageiaSyncExt.findIsos() self.fillList.setup(self.nameWithPath, self.password,self.destination) self.fillList.endSignal.connect(self.closeFill) self.fillList.start() # Reset the button self.selectAll.setText(self.tr("Select &All")) self.selectAllState=True def lvMessage( self,message): # Add a line in the logview self.lvText.append(message) def renameDir(self): # Choose the directory where isos are stored directory = QFileDialog.getExistingDirectory(self, self.tr('Select a directory'),self.destination) self.rd.chooseDir.setText(directory) def rename(self): # rename old isos and directories to a new release self.rd=renameDialog() loc=[] loc=self.location.split('/') self.rd.oldRelease.setText(loc[-1]) self.rd.chooseDir.setText(self.destination) answer=self.rd.exec_() if answer: nbf, nbr = mageiaSyncExt.rename(self.rd.chooseDir.text(),\ self.rd.oldRelease.text(),str(self.rd.newRelease.text()),\ 0,0) returnMsg=(self.tr("Renaming {0} files and {1} directories")).format(nbf, nbr) self.lvMessage(returnMsg) self.updateList() self.rd.close() def prefs(self): # From the menu entry self.pd=prefsDialog() if self.password !="": code,list=mageiaSyncExt.findRelease('rsync://'+self.user+'@bcd.mageia.org/isos/', self.password) if code==0: for item in list: self.pd.release.addItem(item) self.pd.release.setCurrentText(self.release) self.pd.password.setText(self.password) self.pd.user.setText(self.user) self.pd.location.setText(self.location) self.pd.selectDest.setText(self.destination) self.pd.bwl.setValue(self.bwl) params=QtCore.QSettings("Mageia","mageiaSync") answer=self.pd.exec_() if answer: params.setValue("release", self.pd.release.currentText()) params.setValue("user",self.pd.user.text()) params.setValue("password",self.pd.password.text()) params.setValue("location",self.pd.location.text()) params.setValue("destination",self.pd.selectDest.text()) params.setValue("bwl",str(self.pd.bwl.value())) self.prefsInit() self.updateList() self.pd.close() def about(self): ad = aboutDialog() answer=ad.exec_() if answer: ad.close() def credits(self): ad = creditsDialog() answer=ad.exec_() if answer: ad.close() def help(self): # Open page in browser l = QDesktopServices.openUrl(QUrl('https://wiki.mageia.org/en/ISO_testing_rsync_tools')) def launchSync(self): dest = QFileInfo(self.destination) if (not dest.exists()) or (not dest.isDir()): self.lvMessage(self.tr("/!\ Local directory {} doesn't exists or isn't accessible. Check mounts or settings.").format(self.destination)) return if not dest.isWritable(): self.lvMessage(self.tr("/!\ Local directory {} isn't writable")) return self.IprogressBar.setEnabled(True) self.stop.setEnabled(True) self.syncGo.setEnabled(False) self.listIsos.setEnabled(False) self.selectAll.setEnabled(False) # Connect the button Stop self.stop.clicked.connect(self.stopSync) self.rsyncThread.params(self.password, self.bwl) remoteRow=-1 for isoIndex in self.listIsos.selectionModel().selectedIndexes(): if remoteRow != isoIndex.row(): remoteRow = isoIndex.row() path = self.modelRemote.data(self.modelRemote.index(remoteRow,0)) name = self.modelRemote.data(self.modelRemote.index(remoteRow,1)) try: # Look for ISO in local list item=self.model.findItems(name,QtCore.Qt.MatchExactly,1)[0] except: # Remote ISO is not yet in local directory. We add it in localList and create the directory self.localAdd(path,name,0) basedir=QtCore.QDir(self.destination) basedir.mkdir(path) item=self.model.findItems(name,QtCore.Qt.MatchExactly,1)[0] row=self.model.indexFromItem(item).row() if self.location == "" : self.nameWithPath='rsync://'+self.user+'@bcd.mageia.org/isos/'+self.release+'/'+path else: self.nameWithPath=self.location+path if (not str(path).endswith('/')): self.nameWithPath+='/' self.rsyncThread.setup(self.nameWithPath, self.destination+'/'+path+'/',row) self.rsyncThread.start() # start the thread def closeFill(self,code): if code==0: # list returned list=self.fillList.getList() for size, date,longName in list: path=longName.split('/') self.add(path[0], path[-1], size,date) elif code==1: self.lvMessage(self.tr("Command rsync not found")) elif code==2: self.lvMessage(self.tr("Error in rsync parameters")) elif code==3: self.lvMessage(self.tr("Unknown error in rsync")) list=self.fillList.getLocal() for path,iso,isoSize in list: self.localAdd(path,iso, isoSize) self.fillList.quit() self.lw.hide() def stopSync(self): self.rsyncThread.stop() self.IprogressBar.setEnabled(False) self.stop.setEnabled(False) self.syncGo.setEnabled(True) self.listIsos.setEnabled(True) self.selectAll.setEnabled(True) def main(self): self.show() # Load or look for intitial parameters self.prefsInit() # look for Isos list and add it to the isoSync list. Update preferences self.updateList() def close(self): self.rsyncThread.stop() exit(0) if __name__=='__main__': app = QApplication(sys.argv) locale = QtCore.QLocale.system().name() qtTranslator = QtCore.QTranslator() if qtTranslator.load("qt_" + locale,QLibraryInfo.location(QLibraryInfo.TranslationsPath)): app.installTranslator(qtTranslator) appTranslator = QtCore.QTranslator() if appTranslator.load("mageiaSync_" + locale,'/usr/share/mageiasync/translations'): app.installTranslator(appTranslator) isosSync = IsosViewer() isosSync.main() sys.exit(app.exec_())