diff --git a/gui/irodsCreateTicket.py b/gui/irodsCreateTicket.py index 88755be06c13c81f2dbf84a72536f9c4f9f890c4..225624f98c3dcc0d44ac067d781f0596531aac4d 100644 --- a/gui/irodsCreateTicket.py +++ b/gui/irodsCreateTicket.py @@ -34,8 +34,8 @@ class irodsCreateTicket(): #gather info idx, path = self.irodsmodel.get_checked() - if path == None: - self.widget.infoLabel.setText("ERROR: No data selected.") + if path == None or self.ic.session.data_objects.exists(path): + self.widget.infoLabel.setText("ERROR: Please select a collection.") self.widget.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor)) self.widget.createTicketButton.setEnabled(True) return diff --git a/gui/irodsTicketLogin.py b/gui/irodsTicketLogin.py index e1a84f2f2892d3d07c4d20fc898e68337347355a..29233f866ef3505eab600baccef98c06f2e1f838 100644 --- a/gui/irodsTicketLogin.py +++ b/gui/irodsTicketLogin.py @@ -1,7 +1,72 @@ from PyQt5 import QtWidgets from PyQt5.uic import loadUi +from utils.irodsConnectorAnonymous import irodsConnectorAnonymous +import os class irodsTicketLogin(): - def __init__(self): + def __init__(self, widget): + self.ic = None + self.coll = None + self.widget = widget + self.widget.connectButton.clicked.connect(self.irodsSession) + self.widget.homeButton.clicked.connect(self.loadTable) + self.widget.collTable.doubleClicked.connect(self.browse) + - print("TODO") + def irodsSession(self): + self.widget.infoLabel.clear() + host = self.widget.serverEdit.text() + token = self.widget.ticketEdit.text() + path = self.widget.pathEdit.text() + + try: + self.ic = irodsConnectorAnonymous(host, token, path) + self.coll = self.ic.getData() + self.loadTable() + except Exception as e: + self.widget.infoLabel.setText("LOGIN ERROR: Check ticket and iRODS path.\n"+repr(e)) + + + def loadTable(self, update = None): + self.widget.infoLabel.clear() + if self.coll == None: + self.widget.infoLabel.setText("No data avalaible. Check ticket and iRODS path.") + return + if update == None or update == False: + update = self.coll + + self.widget.collTable.setRowCount(0) + self.widget.collTable.setRowCount(len(update.subcollections)+len(update.data_objects)) + row = 0 + for subcoll in update.subcollections: + self.widget.collTable.setItem(row, 0, + QtWidgets.QTableWidgetItem(os.path.dirname(subcoll.path))) + self.widget.collTable.setItem(row, 1, + QtWidgets.QTableWidgetItem(subcoll.name+"/")) + self.widget.collTable.setItem(1, 2, QtWidgets.QTableWidgetItem("")) + self.widget.collTable.setItem(1, 3, QtWidgets.QTableWidgetItem("")) + row = row + 1 + for obj in update.data_objects: + self.widget.collTable.setItem(row, 0, + QtWidgets.QTableWidgetItem(os.path.dirname(obj.path))) + self.widget.collTable.setItem(row, 1, QtWidgets.QTableWidgetItem(obj.name)) + self.widget.collTable.setItem(row, 2, QtWidgets.QTableWidgetItem(str(obj.size))) + self.widget.collTable.setItem(row, 3, QtWidgets.QTableWidgetItem(str(obj.checksum))) + self.widget.collTable.setItem(row, 4, + QtWidgets.QTableWidgetItem(str(obj.modify_time))) + row = row+1 + self.widget.collTable.resizeColumnsToContents() + + + def browse(self, index): + self.widget.infoLabel.clear() + col = index.column() + row = index.row() + if self.widget.collTable.item(row, 0).text() != '': + path = self.widget.collTable.item(row, 0).text() + item = self.widget.collTable.item(row, 1).text() + if item.endswith('/'): + item = item[:-1] + if self.ic.session.collections.exists(path+'/'+item): + coll = self.ic.session.collections.get(path+'/'+item) + self.loadTable(update = coll) diff --git a/gui/mainmenu.py b/gui/mainmenu.py index 67ee8ad7d5783401bf3501d67de3afe28d318016..40ee7d01e9be79db779a27d6f73ec0e64e653f2e 100644 --- a/gui/mainmenu.py +++ b/gui/mainmenu.py @@ -29,7 +29,7 @@ class mainmenu(QMainWindow): if ic == None and ienv == None: ticketAccessWidget = loadUi("gui/ui-files/tabTicketAccess.ui") self.tabWidget.addTab(ticketAccessWidget, "Ticket Access") - self.ticketAccessTab = irodsTicketLogin() + self.ticketAccessTab = irodsTicketLogin(ticketAccessWidget) else: self.ic = ic self.ienv = ienv diff --git a/gui/ui-files/tabTicketAccess.ui b/gui/ui-files/tabTicketAccess.ui index acd3bc5babf1fb9f1b08cb5ce7539b3cd001ee84..8e981bc6eebd4844b7ff20a4a1104e8415bd8be5 100644 --- a/gui/ui-files/tabTicketAccess.ui +++ b/gui/ui-files/tabTicketAccess.ui @@ -36,34 +36,80 @@ QLineEdit background-color: rgb(85, 87, 83); } +QLabel#infoLabel +{ + color: rgb(217, 174, 23); +} + </string> </property> <layout class="QVBoxLayout" name="verticalLayout"> <item> <layout class="QFormLayout" name="formLayout"> - <item row="0" column="0"> + <item row="1" column="0"> <widget class="QLabel" name="label"> <property name="text"> <string>Ticket:</string> </property> </widget> </item> - <item row="1" column="0"> + <item row="2" column="0"> <widget class="QLabel" name="label_2"> <property name="text"> - <string>Path</string> + <string>iRODS path: </string> </property> </widget> </item> - <item row="0" column="1"> + <item row="1" column="1"> <widget class="QLineEdit" name="ticketEdit"/> </item> - <item row="1" column="1"> + <item row="2" column="1"> <widget class="QLineEdit" name="pathEdit"/> </item> + <item row="0" column="0"> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string>iRODS server: </string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QLineEdit" name="serverEdit"/> + </item> + <item row="3" column="1"> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="connectButton"> + <property name="text"> + <string>Connect</string> + </property> + </widget> + </item> + </layout> + </item> </layout> </item> + <item> + <widget class="QLabel" name="infoLabel"> + <property name="text"> + <string/> + </property> + </widget> + </item> <item> <spacer name="verticalSpacer"> <property name="orientation"> @@ -81,7 +127,10 @@ background-color: rgb(85, 87, 83); </spacer> </item> <item> - <widget class="QTableWidget" name="tableWidget"> + <widget class="QTableWidget" name="collTable"> + <property name="sizeAdjustPolicy"> + <enum>QAbstractScrollArea::AdjustToContents</enum> + </property> <property name="editTriggers"> <set>QAbstractItemView::NoEditTriggers</set> </property> @@ -101,6 +150,11 @@ background-color: rgb(85, 87, 83); <string>Name</string> </property> </column> + <column> + <property name="text"> + <string>Size</string> + </property> + </column> <column> <property name="text"> <string>Checksum</string> @@ -115,6 +169,17 @@ background-color: rgb(85, 87, 83); </item> <item> <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <widget class="QPushButton" name="homeButton"> + <property name="text"> + <string/> + </property> + <property name="icon"> + <iconset> + <normaloff>../icons/home.png</normaloff>../icons/home.png</iconset> + </property> + </widget> + </item> <item> <spacer name="horizontalSpacer"> <property name="orientation"> @@ -129,7 +194,14 @@ background-color: rgb(85, 87, 83); </spacer> </item> <item> - <widget class="QPushButton" name="pushButton"> + <widget class="QPushButton" name="downloadButton"> + <property name="text"> + <string>Download all</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="downloadSelectionButton"> <property name="text"> <string>Download selected</string> </property> diff --git a/utils/irodsConnectorAnonymous.py b/utils/irodsConnectorAnonymous.py new file mode 100644 index 0000000000000000000000000000000000000000..eea67d55c13feb34d366505cf39a2cb9e59e52a2 --- /dev/null +++ b/utils/irodsConnectorAnonymous.py @@ -0,0 +1,202 @@ +from irods.session import iRODSSession +from irods.ticket import Ticket +from irods.exception import CollectionDoesNotExist + +import os +from base64 import b64decode +from shutil import disk_usage +import hashlib +import logging + +RED = '\x1b[1;31m' +DEFAULT = '\x1b[0m' +YEL = '\x1b[1;33m' +BLUE = '\x1b[1;34m' + +class irodsConnectorAnonymous(): + def __init__(self, host, ticket, path): + """ + iRODS anonymous login. + Input: + server: iRODS server + ticket: string + path: iRODS path the ticket grants access to + + """ + self.__name__="irodsConnectorAnonymous" + if path.endswith('/'): + path = path[:-1] + if not path.startswith("/"): + raise Exception("Not a valid iRODS path.") + zone = path.split('/')[1] + self.session = iRODSSession(user='anonymous', + password='', + zone=zone, + port=1247, + host=host) + self.token = ticket + self.path = path + + def getData(self): + ticket = Ticket(self.session, self.token) + ticket.supply() + try: + item = self.session.collections.get(self.path) + return item + except: + raise + + def getUserInfo(self): + pass + + def getPermissions(self, iPath): + pass + + def setPermissions(self, rights, user, path, zone, recursive = False): + pass + + def ensureColl(self, collPath): + pass + + def search(self, keyVals = None): + pass + + def listResources(self): + pass + + def getResource(self, resource): + ''' + Raises: + irods.exception.ResourceDoesNotExist + ''' + return self.session.resources.get(resource) + + def resourceSize(self, resource): + """ + Returns the available space left on a resource in bytes + resource: Name of the resource + Throws: ResourceDoesNotExist if resource not known + AttributeError if 'free_space' not set + """ + try: + size = self.session.resources.get(resource).free_space + return size + except Exception as error: + logging.info('RESOURCE ERROR: Either resource does not exist or size not set.', + exc_info=True) + raise error("RESOURCE ERROR: Either resource does not exist or size not set.") + + + def uploadData(self, source, destination, resource, size, buff = 1024**3, + force = False, diffs = []): + pass + + def downloadData(self, source, destination, size, buff = 1024**3, force = False, diffs=[]): + ''' + Download object or collection. + source: iRODS collection or data object + destination: absolute path to download folder + size: size of data to be downloaded in bytes + buff: buffer on resource that should be left over + force: If true, do not calculate storage capacity on destination + diffs: output of diff functions + ''' + logging.info('iRODS DOWNLOAD: '+str(source)+'-->'+destination) + options = {kw.FORCE_FLAG_KW: '', kw.REG_CHKSUM_KW: ''} + + if destination.endswith(os.sep): + destination = destination[:len(destination)-1] + if not os.path.isdir(destination): + logging.info('DOWNLOAD ERROR: destination path does not exist or is not directory', + exc_info=True) + raise FileNotFoundError( + "ERROR iRODS download: destination path does not exist or is not directory") + if not os.access(destination, os.W_OK): + logging.info('DOWNLOAD ERROR: No rights to write to destination.', + exc_info=True) + raise PermissionError("ERROR iRODS download: No rights to write to destination.") + + if diffs == []:#Only download if not present or difference in files + if self.session.data_objects.exists(source.path): + (diff, onlyFS, onlyIrods, same) = self.diffObjFile(source.path, + os.path.join( + destination, os.path.basename(source.path)), + scope="checksum") + elif self.session.collections.exists(source.path): + subdir = os.path.join(destination, source.name) + if not os.path.isdir(os.path.join(destination, source.name)): + os.mkdir(os.path.join(destination, source.name)) + + (diff, onlyFS, onlyIrods, same) = self.diffIrodsLocalfs( + source, subdir, scope="checksum") + else: + raise FileNotFoundError("ERROR iRODS upload: not a valid source path") + else: + (diff, onlyFS, onlyIrods, same) = diffs + + if not force:#Check space on destination + try: + space = disk_usage(destination).free + if int(size) > (int(space)-buff): + logging.info('DOWNLOAD ERROR: Not enough space on disk.', + exc_info=True) + raise ValueError('ERROR iRODS download: Not enough space on disk.') + if buff < 0: + logging.info('DOWNLOAD ERROR: Negative disk buffer.', exc_info=True) + raise BufferError('ERROR iRODS download: Negative disk buffer.') + except Exception as error: + logging.info('DOWNLOAD ERROR', exc_info=True) + raise error() + + if self.session.data_objects.exists(source.path) and len(diff+onlyIrods) > 0: + try: + logging.info("IRODS DOWNLOADING object:"+ source.path+ + "to "+ destination) + self.session.data_objects.get(source.path, + local_path=os.path.join(destination, source.name), **options) + return + except: + logging.info('DOWNLOAD ERROR: '+source.path+"-->"+destination, + exc_info=True) + raise + + try: #collections/folders + subdir = os.path.join(destination, source.name) + logging.info("IRODS DOWNLOAD started:") + for d in diff: + #upload files to distinct data objects + logging.info("REPLACE: "+d[1]+" with "+d[0]) + self.session.data_objects.get(d[0], local_path=d[1], **options) + + for IO in onlyIrods: #can contain files and folders + #Create subcollections and upload + sourcePath = source.path + "/" + IO + locO = IO.replace("/", os.sep) + destPath = os.path.join(subdir, locO) + if not os.path.isdir(os.path.dirname(destPath)): + os.makedirs(os.path.dirname(destPath)) + logging.info('INFO: Downloading '+sourcePath+" to "+destPath) + self.session.data_objects.get(sourcePath, local_path=destPath, **options) + except: + logging.info('DOWNLOAD ERROR', exc_info=True) + raise + + + def addMetadata(self, items, key, value, units = None): + pass + + def updateMetadata(self, items, key, value, units = None): + pass + + def deleteMetadata(self, items, key, value, units): + pass + + def deleteData(self, item): + pass + + def executeRule(self, ruleFile, params, output='ruleExecOut'): + pass + + + def createTicket(self, path, expiryString=""): + pass