def __CleanUpArchiveFiles(fileList): ''' Cleanup durchführen, von Dateien die zwar für den Upload erstellt wurden wie z.B. Plots, aber nach dem Zippen nun nicht mehr benötigt werden. @param fileList: Dict mit den Dateien, welche im ZIP hinterlegt wurden und nun ggf. gelöscht werden können. @type fileList: dict ''' for each in fileList: if each.get(u"removeFileAfterZipped", False): toRemove = each.get(u'file') try: if toRemove is not None and os.path.exists(toRemove): os.remove(toRemove) # Leere erstellte Parent-Ordner löschen (maximal zwei Ebenen nach oben!) parentRemoveCounter = 0 tmpDir = os.path.dirname(toRemove) while (parentRemoveCounter < 2): parentRemoveCounter += 1 shutil.rmtree(tmpDir, True) # Nächstes Parent ermitteln tmpDir = os.path.dirname(tmpDir) except BaseException as ex: EPrint( _(u"Löschen der Datei {0} nicht möglich: {1}").format( toRemove, ex))
def FilterShortName(shortName): ''' Entfernt unerlaubte Sonderzeichen aus dem übergebenen String und ersetzt diese ggf. mit einem Unterstrich "_". Anschließend werden Unterstriche am Anfang und Ende des Strings entfernt. @param shortName: zu filternder String. @type shortName: str @return: bereinigter String. @rtype: str ''' converter = PackageNameToATXTestCaseShortName() validShortname = converter.GetValidShortName(shortName) # Wenn der Shortname länger ist als die erlaubten 128 Zeichen (PlannedFolder _XY Zusatz-Counter # bedenken), dann als Fallback versuchen den Namen zu kürzen indem mögliche Unterstriche # komplett entfernt werden. maxLength = 128 - 3 if len(validShortname) > maxLength: validShortname = AutoShortnameUnderscoreCut(validShortname, maxLength) if len(validShortname) <= maxLength: WPrint(_(u"Maximale Länge von 125 Zeichen beim ATX-Name '{0}' überschritten. " u"Der Name wurde automatisch gekürzt durch das Entfernen von Unterstrichen"). format(shortName)) if len(validShortname) > maxLength: EPrint(_(u"ATX-Name '{0}' ist zu lang - erlaubt sind max. 125 Zeichen!").format(shortName)) return validShortname
def __AutoUpdate(atxReportTemplateDir, proxies, useHttps, hostUrl, port, contextUrl, authKey): ''' Lädt die passende ATX Version vom Server und entpackt diese in Templateverzeichnis des Workspaces und aktualisert dann nachfolgenden Module für die Weiterverarbeitung. @param atxReportTemplateDir: Verzeichnis in welchem der neue ATX-Generator liegen soll. @type atxReportTemplateDir: str @param proxies: Dict mit dem Mapping der Protokolle bei Verwendung eines Proxies @type proxies: dict @param useHttps: True, wenn eine Https-Verbindung verwendet werden soll, sonst False. @type useHttps: boolean @param hostUrl: Haupt-URL @type hostUrl: str @param port: Port @type port: integer @param contextUrl: Context-URL (kann u.U. auch leer sein) @type contextUrl: str @param authKey: Authentifizierungsschlüssel für den Download @type authKey: str @return: True, wenn das AutoUpdate erfolgreich war, sonst False. @rtype: boolean ''' # Nach erfolgreichem Download und entpacken die Module aktualisieren if __DownloadAndUnZipATXMakoInTemplateDir(atxReportTemplateDir, proxies, useHttps, hostUrl, port, contextUrl, authKey): # ReportManager aktualisieren from lib.report.ReportManager import ReportManager ReportManager().UpdateHandler() SPrint(_(u"ATX AutoUpdate ist erfolgt.")) return True else: EPrint(_(u"ATX AutoUpdate fehlgeschlagen!")) return False
def __ComputePlotRefPaths(self, plots, rootNode, stepShortName): """ Ermittelt zu den übergebenen Plots und dem TEST-STEP die Zuweisung der Dateien über den RefPath der TEST-STEPS. :param plots: Liste der Plot-Dateien, welche im TEST-STEP gefunden wurden. :type plots: list :param rootNode: Root-Node, welcher durchsucht werden solle :type rootNode: Node :param stepShortName: eindeutiger TEST-STEP Shortname zu welchem die Plots gesucht werden sollen :type stepShortName: str """ if len(plots) > 0: def GetRefTestStepPath(steps): """ Ermittelt den RefPath des gesuchten TEST-STEP-Shortnames. :param steps: Liste mit den enthaltenen TEST-STEPS auf der Ebene :type steps: list :return: None oder den Ref-Path des TEST-STEPS. :rtype: str """ result = None for eachStep in steps: if eachStep[u'SHORT-NAME'] == stepShortName: result = eachStep[u'ORIGIN-REF'][u'#'] break else: result = GetRefTestStepPath( eachStep.get(u'*TEST-STEPS', [])) return result refPath = GetRefTestStepPath(rootNode.GetList()[u'reportSteps']) if refPath is None: EPrint(u'Compute ORIGIN TEST-STEP RefPath failed!') else: if refPath not in self.__plots: self.__plots[refPath] = [] self.__plots[refPath].extend(plots)
def __GetXmlRoot(self, data): ''' Erstellt den Root-Knoten der ATX-XML und hängt die Daten unterhalb des Root-Knotens an. @param data: Daten, welche als XML rausgeschrieben werden sollen. @type data: dict oder list @return: Root-Konten des ATX-XML Dokumentes @rtype: etree.Element ''' try: root = etree.Element( u'ATX', nsmap={ u'xsi': u'http://www.w3.org/2001/XMLSchema-instance', None: u'http://www.asam.net/schema/ATX/r1.0' }) root.append( etree.Comment(u"Generator version: {0}".format( self.__version))) self.__AddElements(root, data) except BaseException as ex: import traceback traceback.print_exc() EPrint(u"Error on: {0}: {1}".format(self.__reportTrfFile, ex)) return root
def __AsyncUpload(self, response, startUploadTime, serverLabel): """ Führt den asynchronen Upload durch. Es erfolgt ein Upload der Daten an TEST-GUIDE, welches dann die Status-URl des Uploads bereitstellt und hier direkt gepollt wird. :param response: HTTP-Antwort, für den verwendeten Upload. :type response: requests.Response :param startUploadTime: Startzeitpunkt des Uploads für die Messung und die Anzeige in der Log-Anzeige. :type startUploadTime: datetime.datetime :param serverLabel: Server, der ausgewählt wurde für die Log-Anzeige. :type serverLabel: str :returns: True, bei erfolgreichem Upload, sonst False. :rtype: boolean """ statusResLink = None visual = ShowInfoOnTaskManager( self.__reportApi, _(u'Upload wird asynchron verarbeitet...')) # Erwartete Rückgabe von TEST-GUIDE # {'ENTRIES':[{'FILE': 'file-upload6523204238481192334.tmp', # 'STATUS': 202, # 'TEXT': '/api/upload-file/status/6'}]} if response.status_code == 202: try: respDict = response.json() except Exception as err: EPrint(u'Read async upload payload failed: {0}'.format( str(err))) respDict = {} responses = respDict.get(u'ENTRIES', []) if responses: statusResLink = responses.pop().get(u'TEXT') else: raise self._CreateUploadError(response.status_code, u'Upload status request failed') if statusResLink: statusUrl = self._GetStatusUrl(statusResLink) # Polling für die Status-Abfrage durchführen. constMinSleepTimeSec = 3 constMaxWaitTimeHours = 7 constMaxWaitTimeInSec = constMaxWaitTimeHours * 60 * 60 waitTimeSec = 0 while waitTimeSec < constMaxWaitTimeInSec: requestStat = requests.get( url=statusUrl, headers={ u'Accept': u'application/{accept}'.format(accept=self.__accept) }, verify=False, proxies=self.__proxies) requestStat.raise_for_status() try: respDict = requestStat.json() except ValueError as err: EPrint(u'Read async upload status failed: {0}'.format( str(err))) respDict = {} status = respDict.get(u'status') if status == u'FINISHED': statusCode = respDict.get(u'statusCode') if statusCode >= 300: raise UploadError(statusCode, u'Upload failed', respDict) else: self.__SuccessUpload(requestStat.content, statusCode, startUploadTime, serverLabel) return True else: statusMsg = respDict.get(u'lastCommandDescription') if statusMsg and visual: visual.SetCaption( _(u'Uploadstatus: {0}').format(statusMsg)) # Minimum-Wartezeit bis zur nächsten Abfrage time.sleep(constMinSleepTimeSec) waitTimeSec += constMinSleepTimeSec else: # Wenn der Statuslink nicht ermittelt werden kann! raise self._CreateUploadError(500, u'Unknown status link') return False
def StartUpload(self): """ Führt den zuvor präparierten Upload aus. :returns: True bei Erfolg, sonst False. :rtype: boolean """ startUploadTime = datetime.datetime.now() serverLabel = _(u'(Server: {0}-{1})').format( self.__serverLabel, self.__projectId) if self.__serverLabel else u'' encoder = MultipartEncoder( fields={ self.__uploadFieldName: ( # TEST-GUIDE (Apache-Commom-Fileupload) unterstützt im Moment nicht # https://tools.ietf.org/html/rfc2231 # deswegen wird der Filename immer kodiert übertragen. quote(self.__uploadFileName.encode('utf-8')), io.open(self.__uploadFilePath, u'rb'), self.__GetContentType(self.__uploadFileName)) }) callback = self._CreateMultipartEncoderMonitorCallback(encoder) monitor = MultipartEncoderMonitor(encoder, callback=callback) logPath = u'{reportDir}\\error.log'.format( reportDir=self.__reportApi.GetReportDir()) logRawPath = u'{reportDir}\\error.raw.log'.format( reportDir=self.__reportApi.GetReportDir()) logRawJsonPath = u'{reportDir}\\error.raw.json'.format( reportDir=self.__reportApi.GetReportDir()) try: try: response = requests.post( url=self.GetTargetUrl(), data=monitor, headers={ u'Accept': u'application/{accept}'.format(accept=self.__accept), u'Content-Type': monitor.content_type }, verify=False, proxies=self.__proxies) response.raise_for_status() if self.__uploadAsync: return self.__AsyncUpload(response, startUploadTime, serverLabel) return self.__SyncUpload(response, startUploadTime, serverLabel) except HTTPError as err: response = err.response # Raw Response sichern with io.open(logRawPath, u'w+', encoding=u'utf-8') as logFile: logFile.write(u'{0}'.format(response.content)) try: logDict = response.json() except: logDict = {} if isinstance(response.reason, bytes): reason = response.reason.decode(u'utf-8', u'replace') else: reason = response.reason raise UploadError(reason, response.status_code, logDict) except UploadError as err: # JSON Response sichern with io.open(logRawJsonPath, u'w+', encoding=u'utf-8') as logFile: logFile.write(u'{0}'.format(err.logEntries)) # Error-Log Messages auswerten und aufbereiten für die Anzeige logMessages = u'\n'.join([ u'Error {0}: {1}'.format(eachLogEntry.get(u'statusCode'), eachLogEntry.get(u'body')) for eachLogEntry in err.logEntries.get(u'messages', []) ]) with io.open(logPath, u'w+', encoding=u'utf-8') as logFile: logFile.write(u'Error {code} - {reason}:\n{msg}\n'.format( code=err.statusCode, reason=err.reason, msg=logMessages)) endUploadTime = datetime.datetime.now() EPrint( _(u'Die ATX-Übertragung [{files}] war erfolgreich {serverLabel} ' u'(Generator v{version}), ' u'die Verarbeitung der Dateien führte jedoch zu Fehlern ({errCode}):\r\n' u'{msg}' u'Error-Log: {logPath}\r\n' u'Uploaddauer: {duration}').format( logPath=logPath, msg=logMessages, errCode=err.statusCode, files=self.__uploadFilePath, version=self.__version, duration=str(endUploadTime - startUploadTime).split( u'.', 1)[0], serverLabel=serverLabel)) return False except RequestException as err: EPrint( _(u'Die ATX-Übertragung [{files}] {serverLabel} konnte nicht statt ' u'finden:\r\n{reason}').format(reason=str(err), files=self.__uploadFilePath, serverLabel=serverLabel)) return False
def ProcessReport(reportApi, isPackageExecution): ''' Führt die Reportgenerierung aus. @param reportApi: Aktuelles Objekt der ReportAPI. @type reportApi: ReportApi @param isPackageExecution: Handelt es sich um ein PackageReport. @type isPackageExecution: bool ''' if not isinstance(isPackageExecution, bool): EPrint(u'isPackageExecution ist kein boolscher Wert!') return # Generierung startet DPrint( LEVEL_NORMAL, _(u"ATX-Dokument aus Report [{1}] mit Generator v{0} " u"wird erstellt...").format(GetVersion(), reportApi.GetDbFile())) startGenerateTime = datetime.datetime.now() try: url = Config.GetSetting(reportApi, u'serverURL') port = int(Config.GetSetting(reportApi, u'serverPort')) contextPath = Config.GetSetting(reportApi, u'serverContextPath') useHttps = Config.GetSetting(reportApi, u'useHttpsConnection') == u'True' proxies = CreateRequestProxySettings(reportApi) serverVersion = GetServerVersion(useHttps, url, port, contextPath, proxies) authKey = Config.GetSetting(reportApi, u'uploadAuthenticationKey') hasServerConnection = (serverVersion != u"0.0.0") hasSameVersion = parse_version(serverVersion) == parse_version( GetVersion()) warnMsgNotUpToDateVer = _( u"Der verwendete ATX-Reportgenerator v{0} ist " u"ungleich der Version {1} welche vom Server gefordert " u"wird!\n" u"Die passende Version steht zum Download bereit unter:\n{2}\n" u"Nach dem Download den Ordner ATX im Workspace im eingestellten " u"Template-Verzeichnis '{3}' ablegen/ersetzen." u".").format( GetVersion(), serverVersion, GetDownloadLinkForATXMako(useHttps, url, port, contextPath, authKey), Api().GetSetting(u"templatePath")) # Warnung mit Downloadlink zur Server-Mako-Version anbieten. if hasServerConnection and not hasSameVersion: WPrint(warnMsgNotUpToDateVer) splitter = reportApi.GetDbDir().rfind(u'\\') zipFileName = reportApi.GetDbDir()[splitter + 1:] try: worker = ConvertReportToATX(reportApi, GetVersion(), isPackageExecution) except EmptyReportException: # Der Report hat keine verwertbaren Testcases erzeugt und daher wird die Generierung # abgebrochen WPrint( _(u'Der Report enthält keine Testfalldaten und wird daher verworfen.' )) return # Wenn Target Verzeichnis nicht bereits angelegt, dann anlegen. targetDir = GetExtendedWindowsPath(reportApi.GetReportDir()) if not os.path.exists(targetDir): os.makedirs(targetDir) atxFileName = u'report.xml' atxFilePath = os.path.join(targetDir, atxFileName) # ATX-Datei anlegen und befüllen. worker.CreateATXXmlFile(atxFilePath) ShowInfoOnTaskManager(reportApi, _(u"Zip-Archiv für Upload wird erstellt...")) zipFileNameWithExtension = u'{0}.zip'.format(zipFileName) fileList = worker.GetFiles() # Zusätzliche Dateien aus dem Report Verzeichnis anziehen: eine Zip erstellen dbDir = reportApi.GetDbDir() reportDir = reportApi.GetReportDir() archiveMiscFiles = Config.GetSetting(reportApi, u'archiveMiscFiles').strip() archiveMiscPrefix = Config.GetSetting( reportApi, u'archiveMiscFilePrefix').strip() additionalFilesZipPath = ScanReportDir( reportApi, Api(), dbDir, archiveMiscFiles, ).CreateZipArchive(archiveMiscPrefix, reportDir) # Ref Pfade sammeln und jeden Testcase mit der Zip verknüpfen if additionalFilesZipPath: refs = [] for item in fileList: if item[u'ref'] not in refs: refs.append(item[u'ref']) fileList.append({ u'file': additionalFilesZipPath, u'ref': item[u'ref'], u'comment': None, u'refPathType': u"TEST-CASE", u'removeFileAfterZipped': False }) # **** ZIP mit ATX Xml, Mapping Xml, TRF usw. erstellen zipArchive = ZipArchive(reportApi, zipFileNameWithExtension, atxFilePath, fileList, worker.GetReviews()) if not zipArchive.Make(): # Fehler bei der Erstellung des Zip EPrint(_(u'Fehler beim Erstellen des Zip-Archivs.')) else: # Temp-Dateien ggf. löschen __CleanUpArchiveFiles(fileList) # Generierung erfolgt endGenerateTime = datetime.datetime.now() DPrint( LEVEL_NORMAL, _(u"ATX-Dokument aus Report [{1}] mit Generator v{0} " u"erzeugt (Dauer: {2}).").format( GetVersion(), reportApi.GetDbFile(), str(endGenerateTime - startGenerateTime).split('.')[0])) # Upload zum Server if Config.GetSetting(reportApi, u'uploadToServer') == u'True': ShowInfoOnTaskManager(reportApi, _(u"Daten werden hochgeladen...")) # Prüfe Option nur Projekte hochladen optionUploadOnlyPrjs = u'uploadOnlyProjectReport' if Config.GetSetting( reportApi, optionUploadOnlyPrjs) == u"True" and isPackageExecution: DPrint( LEVEL_NORMAL, _(u"ATX Upload übersprungen"), _(u"Option '{0}' ist aktiviert.").format( optionUploadOnlyPrjs)) return # Prüfe Verbindung if not hasServerConnection: EPrint( _(u"ATX Upload abgebrochen! Keine Verbindung zum Server möglich, bitte " u"Einstellungen überprüfen.")) return # Versionsabgleich -> bei Fehler abbrechen if not hasSameVersion: EPrint( _(u"ATX Upload abgebrochen! {0}").format( warnMsgNotUpToDateVer)) return uploader = UploadManager(reportApi, GetVersion(), u'file-upload', zipFileNameWithExtension, zipArchive.GetZipFilePath(), u'json', url, port, useHttps=useHttps, contextPath=contextPath) if uploader.StartUpload(): # Wenn alles erfolgreich war und gewünscht, dann Dateien wieder löschen if Config.GetSetting(reportApi, u'cleanAfterSuccessUpload') == u'True': ShowInfoOnTaskManager( reportApi, _(u"Aufräumarbeiten werden durchgeführt...")) shutil.rmtree(targetDir, True) except SameNameError as ne: errorMessage = _( u'Der Name {0} wird sowohl für eine Datei und für einen Ordner auf der ' u'selben Dateiebene verwendet. Dies ist für ATX-Darstellung nicht ' u'zulässig.' u'Bitte benennen Sie den Ordner oder das Package um.').format(ne) with io.open(os.path.join(u'\\\\?\\' + reportApi.GetReportDir(), u'error.log'), u'w', encoding=u"utf-8") as fh: fh.write(errorMessage) EPrint(errorMessage) except BaseException as ex: import traceback EPrint(u'Exception in ReportPackage:\r\n{0}\r\n{1}'.format( ex, traceback.format_exc()))
def __InitProcessReport(reportApi, isPackageExecution): ''' Initialisiert die Reportgenerierung. @param reportApi: Aktuelles Objekt der ReportAPI. @type reportApi: ReportApi @param isPackageExecution: Handelt es sich um ein PackageReport. @type isPackageExecution: bool ''' # Thread Lock setzen if not hasattr(Api(), "atxUpdateEvent"): Api().atxUpdateEvent = threading.BoundedSemaphore() try: atxReportTemplateDir = os.path.join(Api().GetSetting("templatePath"), "ATX") url = Config.GetSetting(reportApi, 'serverURL') port = Config.Cast2Int(Config.GetSetting(reportApi, 'serverPort'), 8085) contextPath = Config.GetSetting(reportApi, 'serverContextPath') isAutoUpdate = Config.GetSetting(reportApi, 'autoATXGeneratorUpdate') == 'True' useHttps = Config.GetSetting(reportApi, 'useHttpsConnection') == 'True' proxies = CreateRequestProxySettings(reportApi) serverVersion = GetServerVersion(useHttps, url, port, contextPath, proxies) uploadToServer = Config.GetSetting(reportApi, 'uploadToServer') serverLabel = Config.GetSetting(reportApi, 'serverLabel') projectId = Config.GetSetting(reportApi, 'projectId') authKey = Config.GetSetting(reportApi, 'uploadAuthenticationKey') # Debugausgabe DPrint( LEVEL_VERBOSE, _(u"ATX-Generator für Report [{0}] wird mit folgenden " u"Eigenschaften gestartet:\n" u"serverLabel: {9}\n" u"projectId: {10}\n" u"serverURL: {1}\n" u"serverPort: {2}\n" u"serverContextPath: {3}\n" u"autoATXGeneratorUpdate: {4}\n" u"useHttpsConnection: {5}\n" u"serverVersion: {6}\n" u"reportGenVersion: {7}\n" u"uploadToServer: {8}\n" u"authKey: {11}\n").format(reportApi.GetDbFile(), url, port, contextPath, isAutoUpdate, useHttps, serverVersion, GetVersion(), uploadToServer, serverLabel, projectId, u"****" if authKey else u"")) hasServerConnection = (serverVersion != u"0.0.0") hasSameVersion = parse_version(serverVersion) == parse_version( GetVersion()) # AutoUpdate ab Version 1.5.0 verfügbar hasAutoUpdateVersion = parse_version(serverVersion) >= parse_version( u"1.5.0") # Wenn Verbindung besteht und Update notwendig und verlangt, dann durchführen. if hasServerConnection and hasAutoUpdateVersion and isAutoUpdate and not hasSameVersion: # Alle ATX-Reportgeneratoren an der Stelle fürs Auto-Update synchronisieren Api().atxUpdateEvent.acquire() SPrint( _(u"ATX AutoUpdate {1} wird gestartet für Update " u"auf Generator v{0} ...").format( serverVersion, (_(u"(Server: {0})").format(serverLabel) if serverLabel else u""))) # Update durchführen if __AutoUpdate(atxReportTemplateDir, proxies, useHttps, url, port, contextPath, authKey): # Neuen Generator mit alter Config in dem Update-Thread anstarten from lib.report.handler.python.PythonHandler import PythonHandler handler = PythonHandler.CheckDir(PythonHandler, atxReportTemplateDir) # Freigeben für nächsten Thread der im folgenden 'handler' aufgerufen wird Api().atxUpdateEvent.release() # Wenn es sich um eine Liste handelt, dann sind Fehlermeldungen hinterlegt. if isinstance(handler, list): EPrint(u"ATX PythonHandler Errors: {0}".format(handler)) else: if reportApi.IsProjectReport(): handler.RenderProject(reportApi) else: handler.RenderPackage(reportApi) else: EPrint(_(u"ATX-Generierung mit Upload war nicht möglich!")) # Freigeben für nächsten Thread Api().atxUpdateEvent.release() else: # Report normal verarbeiten ProcessReport(reportApi, isPackageExecution) except BaseException as err: EPrint(u"InitProcessReport failed: {0}".format(err))
def __DownloadAndUnZipATXMakoInTemplateDir(targetZipDir, proxies, useHttps, hostUrl, port, contextUrl, authKey): ''' Downloaded das passende Mako vom Server und entpackt die Version im Workspace Templateverzeichnis und entfernt wieder die Zip. @param targetZipDir: Verzeichnis in welches das Zip entpackt werden soll. @type targetZipDir: str @param proxies: Dict mit dem Mapping der Protokolle bei Verwendung eines Proxies @type proxies: dict @param useHttps: True, wenn eine Https-Verbindung verwendet werden soll, sonst False. @type useHttps: boolean @param hostUrl: Haupt-URL @type hostUrl: str @param port: Port @type port: integer @param contextUrl: Context-URL (kann u.U. auch leer sein) @type contextUrl: str @param authKey: Authentifizierungsschlüssel für den Download @type authKey: str @return: True, wenn Download erfolgreich, sonst False. @rtype: boolean ''' try: # Download der aktuellen Mako in maximal 90s . response = requests.get(GetDownloadLinkForATXMako( useHttps, hostUrl, port, contextUrl, authKey), timeout=90, verify=False, proxies=proxies) # Download downloadTarget = os.path.join(Api().GetSetting(u"templatePath"), u"ATX.zip") with open(downloadTarget, u'wb') as output: output.write(response.content) # Bestands-Generator feststellen if os.path.isdir(targetZipDir): # Wenn Verzeichnis bereits vorhanden, dann Python-Inhalte löschen, # damit Templates nicht zugemüllt wird files = glob.glob(os.path.join(targetZipDir, "*.py*")) try: for each in files: os.remove(each) # Python 3 File-Cache löschen pyCacheDir = os.path.join(targetZipDir, "__pycache__") if os.path.exists(pyCacheDir): shutil.rmtree(pyCacheDir, ignore_errors=False) except BaseException as err: WPrint(u"ATX folder file could not removed: {0}".format(err)) # Fallback, falls das einzelne Löschen nicht möglich ist. if os.path.exists(targetZipDir): shutil.rmtree(targetZipDir, ignore_errors=False) # Unzip with zipfile.ZipFile(downloadTarget) as zf: zf.extractall(targetZipDir) # Remove Zip-File os.remove(downloadTarget) return True except BaseException as err: EPrint(u"ATX Zip Update failed: {0}".format(err)) return False