From 7093716fad002ff45afd29b95a55ea7bc03b9712 Mon Sep 17 00:00:00 2001 From: Kim Wall <30846798+kimble4@users.noreply.github.com> Date: Fri, 23 Dec 2022 22:23:43 +0000 Subject: [PATCH 01/13] Add SFTP support to SeriesMgr Also fixes a bug where team results would be uploaded to the same filename as individual results. --- SeriesMgr/FtpWriteFile.py | 83 ++++++++++++++++++++++++++++----------- SeriesMgr/SeriesModel.py | 1 + SeriesMgr/TeamResults.py | 2 +- 3 files changed, 63 insertions(+), 23 deletions(-) diff --git a/SeriesMgr/FtpWriteFile.py b/SeriesMgr/FtpWriteFile.py index a592bdbbf..b6bf90bd7 100644 --- a/SeriesMgr/FtpWriteFile.py +++ b/SeriesMgr/FtpWriteFile.py @@ -3,6 +3,7 @@ import os import sys import ftplib +import paramiko import datetime import threading import webbrowser @@ -13,25 +14,51 @@ def lineno(): """Returns the current line number in our program.""" return inspect.currentframe().f_back.f_lineno - -def FtpWriteFile( host, user = 'anonymous', passwd = 'anonymous@', timeout = 30, serverPath = '.', fileName = '', file = None ): - ftp = ftplib.FTP( host, timeout = timeout ) - ftp.login( user, passwd ) - if serverPath and serverPath != '.': - ftp.cwd( serverPath ) - fileOpened = False - if file is None: - file = open(fileName, 'rb') - fileOpened = True - ftp.storbinary( 'STOR {}'.format(os.path.basename(fileName)), file ) - ftp.quit() - if fileOpened: - file.close() -def FtpWriteHtml( html_in ): +class CallCloseOnExit: + def __init__(self, obj): + self.obj = obj + def __enter__(self): + return self.obj + def __exit__(self, exc_type, exc_val, exc_tb): + self.obj.close() + +def FtpWriteFile( host, user = 'anonymous', passwd = 'anonymous@', timeout = 30, serverPath = '.', fileName = '', file = None, useSftp = False, sftpPort = 22): + if useSftp: + with CallCloseOnExit(paramiko.SSHClient()) as ssh: + ssh.set_missing_host_key_policy( paramiko.AutoAddPolicy() ) + ssh.load_system_host_keys() + ssh.connect( host, sftpPort, user, passwd ) + + with CallCloseOnExit(ssh.open_sftp()) as sftp: + fileOpened = False + if file is None: + file = open(fileName, 'rb') + fileOpened = True + sftp.putfo( + file, + serverPath + os.path.basename(fileName) + ) + if fileOpened: + file.close() + else: + ftp = ftplib.FTP( host, timeout = timeout ) + ftp.login( user, passwd ) + if serverPath and serverPath != '.': + ftp.cwd( serverPath ) + fileOpened = False + if file is None: + file = open(fileName, 'rb') + fileOpened = True + ftp.storbinary( 'STOR {}'.format(os.path.basename(fileName)), file ) + ftp.quit() + if fileOpened: + file.close() + +def FtpWriteHtml( html_in, team = False ): Utils.writeLog( 'FtpWriteHtml: called.' ) modelFileName = Utils.getFileName() if Utils.getFileName() else 'Test.smn' - fileName = os.path.basename( os.path.splitext(modelFileName)[0] + '.html' ) + fileName = os.path.basename( os.path.splitext(modelFileName)[0] + ('-Team.html' if team else '.html') ) defaultPath = os.path.dirname( modelFileName ) with open(os.path.join(defaultPath, fileName), 'w') as fp: fp.write( html_in ) @@ -41,6 +68,7 @@ def FtpWriteHtml( html_in ): user = getattr( model, 'ftpUser', '' ) passwd = getattr( model, 'ftpPassword', '' ) serverPath = getattr( model, 'ftpPath', '' ) + useSftp = getattr( model, 'useSftp', False ) with open( os.path.join(defaultPath, fileName), 'rb') as file: try: @@ -49,7 +77,8 @@ def FtpWriteHtml( html_in ): passwd = passwd, serverPath = serverPath, fileName = fileName, - file = file ) + file = file, + useSftp = useSftp) except Exception as e: Utils.writeLog( 'FtpWriteHtml Error: {}'.format(e) ) return e @@ -59,16 +88,19 @@ def FtpWriteHtml( html_in ): #------------------------------------------------------------------------------------------------ class FtpPublishDialog( wx.Dialog ): - fields = ['ftpHost', 'ftpPath', 'ftpUser', 'ftpPassword', 'urlPath'] - defaults = ['', '', 'anonymous', 'anonymous@' 'http://'] + fields = ['ftpHost', 'ftpPath', 'ftpUser', 'ftpPassword', 'urlPath', 'useSftp'] + defaults = ['', '', 'anonymous', 'anonymous@', 'http://', False] + team = False - def __init__( self, parent, html, id = wx.ID_ANY ): + def __init__( self, parent, html, team = False, id = wx.ID_ANY ): super().__init__( parent, id, "Ftp Publish Results", style=wx.DEFAULT_DIALOG_STYLE|wx.TAB_TRAVERSAL ) self.html = html + self.team = team bs = wx.GridBagSizer(vgap=0, hgap=4) + self.useSftp = wx.CheckBox( self, label=_("Use SFTP Protocol (on port 22)") ) self.ftpHost = wx.TextCtrl( self, size=(256,-1), style=wx.TE_PROCESS_ENTER, value='' ) self.ftpPath = wx.TextCtrl( self, size=(256,-1), style=wx.TE_PROCESS_ENTER, value='' ) self.ftpUser = wx.TextCtrl( self, size=(256,-1), style=wx.TE_PROCESS_ENTER, value='' ) @@ -79,8 +111,15 @@ def __init__( self, parent, html, id = wx.ID_ANY ): self.refresh() + + row = 0 border = 8 + + bs.Add( self.useSftp, pos=(row,1), span=(1,1), border = border, flag=wx.RIGHT|wx.TOP|wx.ALIGN_LEFT ) + + row += 1 + bs.Add( wx.StaticText( self, label=_("Ftp Host Name:")), pos=(row,0), span=(1,1), border = border, flag=wx.LEFT|wx.TOP|wx.ALIGN_RIGHT|wx.ALIGN_CENTRE_VERTICAL ) bs.Add( self.ftpHost, pos=(row,1), span=(1,1), border = border, flag=wx.RIGHT|wx.TOP|wx.ALIGN_LEFT ) @@ -130,7 +169,7 @@ def urlPathChanged( self, event = None ): else: if not url.endswith( '/' ): url += '/' - fileName = os.path.basename( os.path.splitext(fileName)[0] + '.html' ) + fileName = os.path.basename( os.path.splitext(fileName)[0] + ( '-Team.html' if self.team else '.html') ) url += fileName self.urlFull.SetLabel( url ) @@ -156,7 +195,7 @@ def setModelAttr( self ): def onOK( self, event ): self.setModelAttr() - e = FtpWriteHtml( self.html ) + e = FtpWriteHtml( self.html, self.team ) if e: Utils.MessageOK( self, 'FTP Publish: {}'.format(e), 'FTP Publish Error' ) else: diff --git a/SeriesMgr/SeriesModel.py b/SeriesMgr/SeriesModel.py index 8ed971c6b..aced150bc 100644 --- a/SeriesMgr/SeriesModel.py +++ b/SeriesMgr/SeriesModel.py @@ -247,6 +247,7 @@ class SeriesModel: ftpUser = '' ftpPassword = '' urlPath = '' + useSftp = False @property def scoreByPoints( self ): diff --git a/SeriesMgr/TeamResults.py b/SeriesMgr/TeamResults.py index d4adf426a..adbdbbd47 100644 --- a/SeriesMgr/TeamResults.py +++ b/SeriesMgr/TeamResults.py @@ -1013,7 +1013,7 @@ def onPublishToFtp( self, event ): return html = io.open( htmlfileName, 'r', encoding='utf-8', newline='' ).read() - with FtpWriteFile.FtpPublishDialog( self, html=html ) as dlg: + with FtpWriteFile.FtpPublishDialog( self, html=html, team=True ) as dlg: dlg.ShowModal() self.callPostPublishCmd( htmlfileName ) From a493282991585c91db6ebb4ea19c2e0ce3786705 Mon Sep 17 00:00:00 2001 From: Kim Wall <30846798+kimble4@users.noreply.github.com> Date: Fri, 23 Dec 2022 22:40:15 +0000 Subject: [PATCH 02/13] Fix bugs in CrossMgr SFTP --- FtpWriteFile.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/FtpWriteFile.py b/FtpWriteFile.py index 553ef79bf..99d041fe1 100644 --- a/FtpWriteFile.py +++ b/FtpWriteFile.py @@ -21,10 +21,10 @@ def lineno(): return inspect.currentframe().f_back.f_lineno class CallCloseOnExit: - def __enter__(self, obj): + def __init__(self, obj): self.obj = obj - return obj - + def __enter__(self): + return self.obj def __exit__(self, exc_type, exc_val, exc_tb): self.obj.close() @@ -89,13 +89,13 @@ def FtpWriteFile( host, user='anonymous', passwd='anonymous@', timeout=30, serve with CallCloseOnExit(paramiko.SSHClient()) as ssh: ssh.set_missing_host_key_policy( paramiko.AutoAddPolicy() ) ssh.load_system_host_keys() - ssh.connect( host, sftpPort, username, passwd ) + ssh.connect( host, sftpPort, user, passwd ) with CallCloseOnExit(ssh.open_sftp()) as sftp: sftp_mkdir_p( sftp, serverPath ) for i, f in enumerate(fname): sftp.put( - filePath, + f, serverPath + '/' + os.path.basename(f), SftpCallback( callback, f, i ) if callback else None ) From 3a3259a36cf17bcd75c5ba7eb4f3ad67728acc5b Mon Sep 17 00:00:00 2001 From: Kim Wall <30846798+kimble4@users.noreply.github.com> Date: Tue, 27 Dec 2022 18:16:59 +0000 Subject: [PATCH 03/13] Selectable FTP port in CrossMgr --- FtpWriteFile.py | 42 ++++++++++++++++++++++++++++++++++-------- Properties.py | 8 ++++---- 2 files changed, 38 insertions(+), 12 deletions(-) diff --git a/FtpWriteFile.py b/FtpWriteFile.py index 99d041fe1..774402f71 100644 --- a/FtpWriteFile.py +++ b/FtpWriteFile.py @@ -4,6 +4,7 @@ import os import sys import webbrowser +import ftplib import ftputil import paramiko from urllib.parse import quote @@ -55,8 +56,15 @@ def sftp_mkdir_p( sftp, remote_directory ): # Create new dirs starting from the last one that existed. for i in range( i_dir_last, len(dirs_exist) ): sftp.mkdir( '/'.join(dirs_exist[:i+1]) ) - -def FtpWriteFile( host, user='anonymous', passwd='anonymous@', timeout=30, serverPath='.', fname='', useSftp=False, sftpPort=22, callback=None ): + +class FtpWithPort(ftplib.FTP): + def __init__(self, host, user, passwd, port): + #Act like ftplib.FTP's constructor but connect to another port. + ftplib.FTP.__init__(self) + self.connect(host, port) + self.login(user, passwd) + +def FtpWriteFile( host, port, user='anonymous', passwd='anonymous@', timeout=30, serverPath='.', fname='', useSftp=False, callback=None ): if isinstance(fname, str): fname = [fname] @@ -89,7 +97,7 @@ def FtpWriteFile( host, user='anonymous', passwd='anonymous@', timeout=30, serve with CallCloseOnExit(paramiko.SSHClient()) as ssh: ssh.set_missing_host_key_policy( paramiko.AutoAddPolicy() ) ssh.load_system_host_keys() - ssh.connect( host, sftpPort, user, passwd ) + ssh.connect( host, port, user, passwd ) with CallCloseOnExit(ssh.open_sftp()) as sftp: sftp_mkdir_p( sftp, serverPath ) @@ -100,7 +108,7 @@ def FtpWriteFile( host, user='anonymous', passwd='anonymous@', timeout=30, serve SftpCallback( callback, f, i ) if callback else None ) else: - with ftputil.FTPHost( host, user, passwd ) as ftp_host: + with ftputil.FTPHost(host, user, passwd, port, session_factory=FtpWithPort) as ftp_host: ftp_host.makedirs( serverPath, exist_ok=True ) for i, f in enumerate(fname): ftp_host.upload_if_newer( @@ -126,6 +134,7 @@ def FtpUploadFile( fname=None, callback=None ): params = { 'host': getattr(race, 'ftpHost', '').strip().strip('\t'), # Fix cut and paste problems. + 'port': getattr(race, 'ftpPort', 21), 'user': getattr(race, 'ftpUser', ''), 'passwd': getattr(race, 'ftpPassword', ''), 'serverPath': getattr(race, 'ftpPath', ''), @@ -350,8 +359,8 @@ def getTitleTextSize( font ): #------------------------------------------------------------------------------------------------ -ftpFields = ['ftpHost', 'ftpPath', 'ftpPhotoPath', 'ftpUser', 'ftpPassword', 'useSftp', 'ftpUploadDuringRace', 'urlPath', 'ftpUploadPhotos'] -ftpDefaults = ['', '', '', 'anonymous', 'anonymous@', False, False, 'http://', False] +ftpFields = ['ftpHost', 'ftpPort', 'ftpPath', 'ftpPhotoPath', 'ftpUser', 'ftpPassword', 'useSftp', 'ftpUploadDuringRace', 'urlPath', 'ftpUploadPhotos'] +ftpDefaults = ['', 21, '', '', 'anonymous', 'anonymous@', False, False, 'http://', False] def GetFtpPublish( isDialog=True ): ParentClass = wx.Dialog if isDialog else wx.Panel @@ -367,8 +376,11 @@ def __init__( self, parent, id=wx.ID_ANY, uploadNowButton=True ): fgs = wx.FlexGridSizer(vgap=4, hgap=4, rows=0, cols=2) fgs.AddGrowableCol( 1, 1 ) - self.useSftp = wx.CheckBox( self, label=_("Use SFTP Protocol (on port 22)") ) + self.useFtp = wx.RadioButton( self, label=_("FTP"), style = wx.RB_GROUP ) + self.useSftp = wx.RadioButton( self, label=_("SFTP (SSH)") ) + self.Bind( wx.EVT_RADIOBUTTON,self.onSelectProtocol ) self.ftpHost = wx.TextCtrl( self, size=(256,-1), style=wx.TE_PROCESS_ENTER, value='' ) + self.ftpPort = wx.lib.intctrl.IntCtrl( self, size=(256,-1), style=wx.TE_PROCESS_ENTER ) self.ftpPath = wx.TextCtrl( self, size=(256,-1), style=wx.TE_PROCESS_ENTER, value='' ) self.ftpUploadPhotos = wx.CheckBox( self, label=_("Upload Photos to Path") ) self.ftpUploadPhotos.Bind( wx.EVT_CHECKBOX, self.ftpUploadPhotosChanged ) @@ -393,12 +405,18 @@ def __init__( self, parent, id=wx.ID_ANY, uploadNowButton=True ): self.cancelBtn = wx.Button( self, wx.ID_CANCEL ) self.Bind( wx.EVT_BUTTON, self.onCancel, self.cancelBtn ) + fgs.Add( wx.StaticText( self, label = _("Protocol")), flag=wx.ALIGN_RIGHT|wx.ALIGN_CENTRE_VERTICAL ) + fgs.Add( self.useFtp, 1, flag=wx.TOP|wx.ALIGN_LEFT) fgs.AddSpacer( 16 ) - fgs.Add( self.useSftp ) + fgs.Add( self.useSftp, 1, flag=wx.TOP|wx.ALIGN_LEFT) + fgs.Add( wx.StaticText( self, label = _("Host Name")), flag=wx.ALIGN_RIGHT|wx.ALIGN_CENTRE_VERTICAL ) fgs.Add( self.ftpHost, 1, flag=wx.TOP|wx.ALIGN_LEFT|wx.EXPAND ) + fgs.Add( wx.StaticText( self, label = _("Port")), flag=wx.ALIGN_RIGHT|wx.ALIGN_CENTRE_VERTICAL ) + fgs.Add( self.ftpPort, 1, flag=wx.TOP|wx.ALIGN_LEFT|wx.EXPAND ) + fgs.Add( wx.StaticText( self, label = _("Upload files to Path")), flag=wx.ALIGN_RIGHT|wx.ALIGN_CENTRE_VERTICAL ) fgs.Add( self.ftpPath, 1, flag=wx.EXPAND ) @@ -459,6 +477,14 @@ def __init__( self, parent, id=wx.ID_ANY, uploadNowButton=True ): fgs.AddSpacer( 4 ) self.SetSizerAndFit( fgs ) fgs.Fit( self ) + + def onSelectProtocol( self, event ): + if self.useSftp.GetValue(): + self.useFtp.SetValue(False) + self.ftpPort.SetValue(22) + else: + self.useFtp.SetValue(True) + self.ftpPort.SetValue(21) def onFtpTest( self, event ): self.commit() diff --git a/Properties.py b/Properties.py index 423dfc056..dc1e950d3 100644 --- a/Properties.py +++ b/Properties.py @@ -817,13 +817,13 @@ def __init__( self, parent, id=wx.ID_ANY, testCallback=None, ftpCallback=None ): self.ftpCallback = ftpCallback if ftpCallback: - ftpBtn = wx.ToggleButton( self, label=_('Configure Ftp') ) + ftpBtn = wx.ToggleButton( self, label=_('Configure FTP') ) ftpBtn.Bind( wx.EVT_TOGGLEBUTTON, ftpCallback ) else: ftpBtn = None explain = [ - wx.StaticText(self,label=_('Choose File Formats to Publish. Select Ftp option to upload files to Ftp server.')), + wx.StaticText(self,label=_('Choose File Formats to Publish. Select FTP option to upload files to (S)FTP server.')), ] font = explain[0].GetFont() fontUnderline = wx.FFont( font.GetPointSize(), font.GetFamily(), flags=wx.FONTFLAG_BOLD ) @@ -831,7 +831,7 @@ def __init__( self, parent, id=wx.ID_ANY, testCallback=None, ftpCallback=None ): fgs = wx.FlexGridSizer( cols=4, rows=0, hgap=0, vgap=1 ) self.widget = [] - headers = [_('Format'), _('Ftp'), _('Note'), ''] + headers = [_('Format'), _('FTP'), _('Note'), ''] for h in headers: st = wx.StaticText(self, label=h) st.SetFont( fontUnderline ) @@ -1265,7 +1265,7 @@ def __init__( self, parent, id=wx.ID_ANY, addEditButton=True ): ('raceOptionsProperties', RaceOptionsProperties, _('Race Options') ), ('rfidProperties', RfidProperties, _('RFID') ), ('webProperties', WebProperties, _('Web') ), - ('ftpProperties', FtpProperties, _('FTP') ), + ('ftpProperties', FtpProperties, _('(S)FTP') ), ('batchPublishProperties', BatchPublishProperties, _('Batch Publish') ), ('gpxProperties', GPXProperties, _('GPX') ), ('notesProperties', NotesProperties, _('Notes') ), From 1b934346bb13d1047bff3c0fef9a3a87b6bde4a6 Mon Sep 17 00:00:00 2001 From: Kim Wall <30846798+kimble4@users.noreply.github.com> Date: Tue, 27 Dec 2022 18:17:26 +0000 Subject: [PATCH 04/13] Update help --- helptxt/Properties.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/helptxt/Properties.md b/helptxt/Properties.md index 89b4fe5b2..da085a4ab 100644 --- a/helptxt/Properties.md +++ b/helptxt/Properties.md @@ -289,7 +289,8 @@ Options for SFTP and FTP upload: Option|Description :-------|:---------- Use SFTP|Check this if you wish to use the SFTP protocol. Otherwise, FTP protocol will be used. -Host Name:Name of the FTP/SFTP host to upload to. In SFTP, CrossMgr also loads hosts from the user's local hosts file (as used by OpenSSH). +Host Name|Name of the FTP/SFTP host to upload to. In SFTP, CrossMgr also loads hosts from the user's local hosts file (as used by OpenSSH). +Port|Port of the FTP/SFTP host to upload to (resets to default after switching between FTP and SFTP). Upload files to Path|The directory path on the host you wish to upload the files into. If blank, files will be uploaded into the root directory. User|FTP/SFTP User name Password|FTP/SFTP Password From 7067c361f1cec24ca0fac13b0bf754c6ffb06770 Mon Sep 17 00:00:00 2001 From: Kim Wall <30846798+kimble4@users.noreply.github.com> Date: Tue, 27 Dec 2022 18:18:30 +0000 Subject: [PATCH 05/13] Selectable FTP port in SeriesMgr --- SeriesMgr/FtpWriteFile.py | 52 +++++++++++++++++++++++++++++++-------- SeriesMgr/Results.py | 2 +- SeriesMgr/SeriesModel.py | 1 + SeriesMgr/TeamResults.py | 2 +- 4 files changed, 45 insertions(+), 12 deletions(-) diff --git a/SeriesMgr/FtpWriteFile.py b/SeriesMgr/FtpWriteFile.py index b6bf90bd7..07b1c6981 100644 --- a/SeriesMgr/FtpWriteFile.py +++ b/SeriesMgr/FtpWriteFile.py @@ -23,12 +23,19 @@ def __enter__(self): def __exit__(self, exc_type, exc_val, exc_tb): self.obj.close() -def FtpWriteFile( host, user = 'anonymous', passwd = 'anonymous@', timeout = 30, serverPath = '.', fileName = '', file = None, useSftp = False, sftpPort = 22): +class FtpWithPort(ftplib.FTP): + def __init__(self, host, user, passwd, port): + #Act like ftplib.FTP's constructor but connect to another port. + ftplib.FTP.__init__(self) + self.connect(host, port) + self.login(user, passwd) + +def FtpWriteFile( host, port, user = 'anonymous', passwd = 'anonymous@', timeout = 30, serverPath = '.', fileName = '', file = None, useSftp = False): if useSftp: with CallCloseOnExit(paramiko.SSHClient()) as ssh: ssh.set_missing_host_key_policy( paramiko.AutoAddPolicy() ) ssh.load_system_host_keys() - ssh.connect( host, sftpPort, user, passwd ) + ssh.connect( host, port, user, passwd ) with CallCloseOnExit(ssh.open_sftp()) as sftp: fileOpened = False @@ -42,7 +49,8 @@ def FtpWriteFile( host, user = 'anonymous', passwd = 'anonymous@', timeout = 30, if fileOpened: file.close() else: - ftp = ftplib.FTP( host, timeout = timeout ) + ftp = ftplib.FTP() + ftp.connect( host, port, timeout = timeout ) ftp.login( user, passwd ) if serverPath and serverPath != '.': ftp.cwd( serverPath ) @@ -65,6 +73,7 @@ def FtpWriteHtml( html_in, team = False ): model = SeriesModel.model host = getattr( model, 'ftpHost', '' ) + port = getattr( model, 'ftpPort', 21 ) user = getattr( model, 'ftpUser', '' ) passwd = getattr( model, 'ftpPassword', '' ) serverPath = getattr( model, 'ftpPath', '' ) @@ -73,6 +82,7 @@ def FtpWriteHtml( html_in, team = False ): with open( os.path.join(defaultPath, fileName), 'rb') as file: try: FtpWriteFile( host = host, + port = port, user = user, passwd = passwd, serverPath = serverPath, @@ -88,20 +98,23 @@ def FtpWriteHtml( html_in, team = False ): #------------------------------------------------------------------------------------------------ class FtpPublishDialog( wx.Dialog ): - fields = ['ftpHost', 'ftpPath', 'ftpUser', 'ftpPassword', 'urlPath', 'useSftp'] - defaults = ['', '', 'anonymous', 'anonymous@', 'http://', False] + fields = ['ftpHost', 'ftpPort', 'ftpPath', 'ftpUser', 'ftpPassword', 'urlPath', 'useSftp'] + defaults = ['', 21, '', 'anonymous', 'anonymous@', 'http://', False] team = False def __init__( self, parent, html, team = False, id = wx.ID_ANY ): - super().__init__( parent, id, "Ftp Publish Results", + super().__init__( parent, id, "(S)FTP Publish Results", style=wx.DEFAULT_DIALOG_STYLE|wx.TAB_TRAVERSAL ) self.html = html self.team = team bs = wx.GridBagSizer(vgap=0, hgap=4) - self.useSftp = wx.CheckBox( self, label=_("Use SFTP Protocol (on port 22)") ) + self.useFtp = wx.RadioButton( self, label=_("FTP"), style = wx.RB_GROUP ) + self.useSftp = wx.RadioButton( self, label=_("SFTP (SSH)") ) + self.Bind( wx.EVT_RADIOBUTTON,self.onSelectProtocol ) self.ftpHost = wx.TextCtrl( self, size=(256,-1), style=wx.TE_PROCESS_ENTER, value='' ) + self.ftpPort = wx.lib.intctrl.IntCtrl( self, size=(256,-1), style=wx.TE_PROCESS_ENTER ) self.ftpPath = wx.TextCtrl( self, size=(256,-1), style=wx.TE_PROCESS_ENTER, value='' ) self.ftpUser = wx.TextCtrl( self, size=(256,-1), style=wx.TE_PROCESS_ENTER, value='' ) self.ftpPassword = wx.TextCtrl( self, size=(256,-1), style=wx.TE_PROCESS_ENTER|wx.TE_PASSWORD, value='' ) @@ -111,19 +124,30 @@ def __init__( self, parent, html, team = False, id = wx.ID_ANY ): self.refresh() - - row = 0 border = 8 + bs.Add( wx.StaticText( self, label=_("Protocol:")), pos=(row,0), span=(1,1), border = border, + flag=wx.LEFT|wx.TOP|wx.ALIGN_RIGHT|wx.ALIGN_CENTRE_VERTICAL ) + bs.Add( self.useFtp, pos=(row,1), span=(1,1), border = border, flag=wx.RIGHT|wx.TOP|wx.ALIGN_LEFT ) + + row += 1 + bs.Add( self.useSftp, pos=(row,1), span=(1,1), border = border, flag=wx.RIGHT|wx.TOP|wx.ALIGN_LEFT ) row += 1 - bs.Add( wx.StaticText( self, label=_("Ftp Host Name:")), pos=(row,0), span=(1,1), border = border, + bs.Add( wx.StaticText( self, label=_("Host Name:")), pos=(row,0), span=(1,1), border = border, flag=wx.LEFT|wx.TOP|wx.ALIGN_RIGHT|wx.ALIGN_CENTRE_VERTICAL ) bs.Add( self.ftpHost, pos=(row,1), span=(1,1), border = border, flag=wx.RIGHT|wx.TOP|wx.ALIGN_LEFT ) + + row += 1 + + bs.Add( wx.StaticText( self, label=_("Port:")), pos=(row,0), span=(1,1), border = border, + flag=wx.LEFT|wx.TOP|wx.ALIGN_RIGHT|wx.ALIGN_CENTRE_VERTICAL ) + bs.Add( self.ftpPort, pos=(row,1), span=(1,1), border = border, flag=wx.RIGHT|wx.TOP|wx.ALIGN_LEFT ) + row += 1 bs.Add( wx.StaticText( self, label=_("Path on Host to Write HTML:")), pos=(row,0), span=(1,1), border = border, flag=wx.LEFT|wx.TOP|wx.ALIGN_RIGHT|wx.ALIGN_CENTRE_VERTICAL ) @@ -161,6 +185,14 @@ def __init__( self, parent, html, team = False, id = wx.ID_ANY ): self.CentreOnParent(wx.BOTH) self.SetFocus() + def onSelectProtocol( self, event ): + if self.useSftp.GetValue(): + self.useFtp.SetValue(False) + self.ftpPort.SetValue(22) + else: + self.useFtp.SetValue(True) + self.ftpPort.SetValue(21) + def urlPathChanged( self, event = None ): url = self.urlPath.GetValue() fileName = Utils.getFileName() diff --git a/SeriesMgr/Results.py b/SeriesMgr/Results.py index 3ed168656..b798e6e25 100644 --- a/SeriesMgr/Results.py +++ b/SeriesMgr/Results.py @@ -708,7 +708,7 @@ def __init__(self, parent): self.refreshButton.Bind( wx.EVT_BUTTON, self.onRefresh ) self.publishToHtml = wx.Button( self, label='Publish to Html' ) self.publishToHtml.Bind( wx.EVT_BUTTON, self.onPublishToHtml ) - self.publishToFtp = wx.Button( self, label='Publish to Html with FTP' ) + self.publishToFtp = wx.Button( self, label='Publish to Html with (S)FTP' ) self.publishToFtp.Bind( wx.EVT_BUTTON, self.onPublishToFtp ) self.publishToExcel = wx.Button( self, label='Publish to Excel' ) self.publishToExcel.Bind( wx.EVT_BUTTON, self.onPublishToExcel ) diff --git a/SeriesMgr/SeriesModel.py b/SeriesMgr/SeriesModel.py index aced150bc..7c8e98341 100644 --- a/SeriesMgr/SeriesModel.py +++ b/SeriesMgr/SeriesModel.py @@ -243,6 +243,7 @@ class SeriesModel: aliasTeamLookup = {} ftpHost = '' + ftpPort = 21 ftpPath = '' ftpUser = '' ftpPassword = '' diff --git a/SeriesMgr/TeamResults.py b/SeriesMgr/TeamResults.py index adbdbbd47..1570d3c02 100644 --- a/SeriesMgr/TeamResults.py +++ b/SeriesMgr/TeamResults.py @@ -628,7 +628,7 @@ def __init__(self, parent): self.refreshButton.Bind( wx.EVT_BUTTON, self.onRefresh ) self.publishToHtml = wx.Button( self, label='Publish to Html' ) self.publishToHtml.Bind( wx.EVT_BUTTON, self.onPublishToHtml ) - self.publishToFtp = wx.Button( self, label='Publish to Html with FTP' ) + self.publishToFtp = wx.Button( self, label='Publish to Html with (S)FTP' ) self.publishToFtp.Bind( wx.EVT_BUTTON, self.onPublishToFtp ) self.publishToExcel = wx.Button( self, label='Publish to Excel' ) self.publishToExcel.Bind( wx.EVT_BUTTON, self.onPublishToExcel ) From ccbc846eb7b7db1eba4d70bef212e941e01ef047 Mon Sep 17 00:00:00 2001 From: Kim Wall <30846798+kimble4@users.noreply.github.com> Date: Tue, 27 Dec 2022 18:18:59 +0000 Subject: [PATCH 06/13] Update Quickstart --- SeriesMgr/helptxt/QuickStart.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SeriesMgr/helptxt/QuickStart.txt b/SeriesMgr/helptxt/QuickStart.txt index 8ef429118..8826387c8 100644 --- a/SeriesMgr/helptxt/QuickStart.txt +++ b/SeriesMgr/helptxt/QuickStart.txt @@ -185,7 +185,7 @@ Use this screen to specify license aliases to fix misspellings in your given Rac Shows the individual SeriesResult for each category. The Refresh button will recompute the results and is necessary if you have changed one of the Race files. -The publish buttons are fairly self-explanitory. __Publish to HTML with FTP__ will use the FTP site and password in the last CrossMgr race file. +The publish buttons are fairly self-explanitory. __Publish to HTML with (S)FTP__ will open a dialog where you can enter FTP/SFTP server details. The __Post Publish Cmd__ is a command that is run after the publish. This allows you to post-process the results (for example, copy them somewhere). As per the Windows shell standard, you can use %* to refer to all files created by SeriesMgr in the publish. From d75b8f1922e013a7ff89d6985f083ad513a2db5c Mon Sep 17 00:00:00 2001 From: Kim Wall <30846798+kimble4@users.noreply.github.com> Date: Tue, 27 Dec 2022 22:29:30 +0000 Subject: [PATCH 07/13] Add FTPS support to CrossMgr --- FtpWriteFile.py | 79 ++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 61 insertions(+), 18 deletions(-) diff --git a/FtpWriteFile.py b/FtpWriteFile.py index 774402f71..2a5c554df 100644 --- a/FtpWriteFile.py +++ b/FtpWriteFile.py @@ -6,6 +6,7 @@ import webbrowser import ftplib import ftputil +import ftputil.session import paramiko from urllib.parse import quote import datetime @@ -57,14 +58,34 @@ def sftp_mkdir_p( sftp, remote_directory ): for i in range( i_dir_last, len(dirs_exist) ): sftp.mkdir( '/'.join(dirs_exist[:i+1]) ) + class FtpWithPort(ftplib.FTP): - def __init__(self, host, user, passwd, port): - #Act like ftplib.FTP's constructor but connect to another port. - ftplib.FTP.__init__(self) - self.connect(host, port) - self.login(user, passwd) - -def FtpWriteFile( host, port, user='anonymous', passwd='anonymous@', timeout=30, serverPath='.', fname='', useSftp=False, callback=None ): + def __init__(self, host, user, passwd, port, timeout): + #Act like ftplib.FTP's constructor but connect to another port. + ftplib.FTP.__init__(self) + #self.set_debuglevel(2) + self.connect(host, port, timeout) + self.login(user, passwd) + +class FtpsWithPort(ftplib.FTP_TLS): + def __init__(self, host, user, passwd, port, timeout): + ftplib.FTP_TLS.__init__(self) + #self.set_debuglevel(2) + self.connect(host, port, timeout) + self.auth() + self.login(user, passwd) + #Switch to secure data connection. + self.prot_p() + + def ntransfercmd(self, cmd, rest=None): + conn, size = ftplib.FTP.ntransfercmd(self, cmd, rest) + if self._prot_p: + conn = self.context.wrap_socket(conn, + server_hostname=self.host, + session=self.sock.session) #reuse ssl session + return conn, size + +def FtpWriteFile( host, port, user='anonymous', passwd='anonymous@', timeout=30, serverPath='.', fname='', protocol='FTP', callback=None ): if isinstance(fname, str): fname = [fname] @@ -72,7 +93,7 @@ def FtpWriteFile( host, port, user='anonymous', passwd='anonymous@', timeout=30, # Normalize serverPath. serverPath = serverPath.strip().replace('\\', '/').rstrip('/') - if not useSftp: + if protocol != 'SFTP': # Stops ftputils from going into an infinite loop by removing leading slashes.. serverPath = serverPath.lstrip('/').lstrip('\\') @@ -93,7 +114,7 @@ def FtpWriteFile( host, port, user='anonymous', passwd='anonymous@', timeout=30, return ''' - if useSftp: + if protocol == 'SFTP': with CallCloseOnExit(paramiko.SSHClient()) as ssh: ssh.set_missing_host_key_policy( paramiko.AutoAddPolicy() ) ssh.load_system_host_keys() @@ -107,8 +128,19 @@ def FtpWriteFile( host, port, user='anonymous', passwd='anonymous@', timeout=30, serverPath + '/' + os.path.basename(f), SftpCallback( callback, f, i ) if callback else None ) - else: - with ftputil.FTPHost(host, user, passwd, port, session_factory=FtpWithPort) as ftp_host: + elif protocol == 'FTPS': + with ftputil.FTPHost(host, user, passwd, port, timeout, session_factory=FtpsWithPort) as ftp_host: + ftp_host.makedirs( serverPath, exist_ok=True ) + for i, f in enumerate(fname): + ftp_host.upload_if_newer( + f, + serverPath + '/' + os.path.basename(f), + (lambda byteStr, fname=f, i=i: callback(byteStr, fname, i)) if callback else None + ) + ftp_host.close() + + else: #default to unencrypted FTP + with ftputil.FTPHost(host, user, passwd, port, timeout, session_factory=FtpWithPort) as ftp_host: ftp_host.makedirs( serverPath, exist_ok=True ) for i, f in enumerate(fname): ftp_host.upload_if_newer( @@ -116,6 +148,7 @@ def FtpWriteFile( host, port, user='anonymous', passwd='anonymous@', timeout=30, serverPath + '/' + os.path.basename(f), (lambda byteStr, fname=f, i=i: callback(byteStr, fname, i)) if callback else None ) + ftp_host.close() def FtpIsConfigured(): with Model.LockRace() as race: @@ -138,7 +171,7 @@ def FtpUploadFile( fname=None, callback=None ): 'user': getattr(race, 'ftpUser', ''), 'passwd': getattr(race, 'ftpPassword', ''), 'serverPath': getattr(race, 'ftpPath', ''), - 'useSftp': getattr(race, 'useSftp', False), + 'protocol': getattr(race, 'ftpProtocol', 'FTP'), 'fname': fname or [], 'callback': callback, } @@ -359,8 +392,8 @@ def getTitleTextSize( font ): #------------------------------------------------------------------------------------------------ -ftpFields = ['ftpHost', 'ftpPort', 'ftpPath', 'ftpPhotoPath', 'ftpUser', 'ftpPassword', 'useSftp', 'ftpUploadDuringRace', 'urlPath', 'ftpUploadPhotos'] -ftpDefaults = ['', 21, '', '', 'anonymous', 'anonymous@', False, False, 'http://', False] +ftpFields = ['ftpHost', 'ftpPort', 'ftpPath', 'ftpPhotoPath', 'ftpUser', 'ftpPassword', 'ftpUploadDuringRace', 'urlPath', 'ftpUploadPhotos'] +ftpDefaults = ['', 21, '', '', 'anonymous', 'anonymous@', False, 'http://', False] def GetFtpPublish( isDialog=True ): ParentClass = wx.Dialog if isDialog else wx.Panel @@ -373,11 +406,14 @@ def __init__( self, parent, id=wx.ID_ANY, uploadNowButton=True ): else: super().__init__( parent, id ) + self.protocol = 'FTP' + fgs = wx.FlexGridSizer(vgap=4, hgap=4, rows=0, cols=2) fgs.AddGrowableCol( 1, 1 ) - self.useFtp = wx.RadioButton( self, label=_("FTP"), style = wx.RB_GROUP ) - self.useSftp = wx.RadioButton( self, label=_("SFTP (SSH)") ) + self.useFtp = wx.RadioButton( self, label=_("FTP (unencrypted)"), style = wx.RB_GROUP ) + self.useFtps = wx.RadioButton( self, label=_("FTPS (FTP with TLS)") ) + self.useSftp = wx.RadioButton( self, label=_("SFTP (SSH file transfer)") ) self.Bind( wx.EVT_RADIOBUTTON,self.onSelectProtocol ) self.ftpHost = wx.TextCtrl( self, size=(256,-1), style=wx.TE_PROCESS_ENTER, value='' ) self.ftpPort = wx.lib.intctrl.IntCtrl( self, size=(256,-1), style=wx.TE_PROCESS_ENTER ) @@ -408,6 +444,8 @@ def __init__( self, parent, id=wx.ID_ANY, uploadNowButton=True ): fgs.Add( wx.StaticText( self, label = _("Protocol")), flag=wx.ALIGN_RIGHT|wx.ALIGN_CENTRE_VERTICAL ) fgs.Add( self.useFtp, 1, flag=wx.TOP|wx.ALIGN_LEFT) fgs.AddSpacer( 16 ) + fgs.Add( self.useFtps, 1, flag=wx.TOP|wx.ALIGN_LEFT) + fgs.AddSpacer( 16 ) fgs.Add( self.useSftp, 1, flag=wx.TOP|wx.ALIGN_LEFT) @@ -480,10 +518,13 @@ def __init__( self, parent, id=wx.ID_ANY, uploadNowButton=True ): def onSelectProtocol( self, event ): if self.useSftp.GetValue(): - self.useFtp.SetValue(False) + self.protocol = 'SFTP' self.ftpPort.SetValue(22) + elif self.useFtps.GetValue(): + self.protocol = 'FTPS' + self.ftpPort.SetValue(21) else: - self.useFtp.SetValue(True) + self.protocol = 'FTP' self.ftpPort.SetValue(21) def onFtpTest( self, event ): @@ -557,6 +598,7 @@ def refresh( self ): else: for f, v in zip(ftpFields, ftpDefaults): getattr(self, f).SetValue( getattr(race, f, v) ) + self.protocol = getattr(race, 'ftpProtocol', '') self.urlPathChanged() self.ftpUploadPhotosChanged() @@ -566,6 +608,7 @@ def commit( self ): if race: for f in ftpFields: setattr( race, f, getattr(self, f).GetValue() ) + setattr( race, 'ftpProtocol', self.protocol) race.urlFull = self.urlFull.GetLabel() race.setChanged() From 8d502e118fb9e02e864784db78f153f1971babf7 Mon Sep 17 00:00:00 2001 From: Kim Wall <30846798+kimble4@users.noreply.github.com> Date: Tue, 27 Dec 2022 22:30:02 +0000 Subject: [PATCH 08/13] Update help --- helptxt/Properties.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helptxt/Properties.md b/helptxt/Properties.md index da085a4ab..cec628582 100644 --- a/helptxt/Properties.md +++ b/helptxt/Properties.md @@ -288,7 +288,7 @@ Options for SFTP and FTP upload: Option|Description :-------|:---------- -Use SFTP|Check this if you wish to use the SFTP protocol. Otherwise, FTP protocol will be used. +Protocol|Select one of FTP, FTPS (FTP with SSL encryption) or SFTP (SSH File Transfer Protocol) Host Name|Name of the FTP/SFTP host to upload to. In SFTP, CrossMgr also loads hosts from the user's local hosts file (as used by OpenSSH). Port|Port of the FTP/SFTP host to upload to (resets to default after switching between FTP and SFTP). Upload files to Path|The directory path on the host you wish to upload the files into. If blank, files will be uploaded into the root directory. From 499b1099188ed17004a2f5e7434ac924316b768f Mon Sep 17 00:00:00 2001 From: Kim Wall <30846798+kimble4@users.noreply.github.com> Date: Tue, 27 Dec 2022 22:30:40 +0000 Subject: [PATCH 09/13] Add FTPS support to SeriesMgr --- SeriesMgr/FtpWriteFile.py | 85 +++++++++++++++++++++++++++++---------- 1 file changed, 64 insertions(+), 21 deletions(-) diff --git a/SeriesMgr/FtpWriteFile.py b/SeriesMgr/FtpWriteFile.py index 07b1c6981..c60f0a693 100644 --- a/SeriesMgr/FtpWriteFile.py +++ b/SeriesMgr/FtpWriteFile.py @@ -22,16 +22,35 @@ def __enter__(self): return self.obj def __exit__(self, exc_type, exc_val, exc_tb): self.obj.close() - + class FtpWithPort(ftplib.FTP): - def __init__(self, host, user, passwd, port): - #Act like ftplib.FTP's constructor but connect to another port. - ftplib.FTP.__init__(self) - self.connect(host, port) - self.login(user, passwd) + def __init__(self, host, user, passwd, port, timeout): + #Act like ftplib.FTP's constructor but connect to another port. + ftplib.FTP.__init__(self) + #self.set_debuglevel(2) + self.connect(host, port, timeout) + self.login(user, passwd) + +class FtpsWithPort(ftplib.FTP_TLS): + def __init__(self, host, user, passwd, port, timeout): + ftplib.FTP_TLS.__init__(self) + #self.set_debuglevel(2) + self.connect(host, port, timeout) + self.auth() + self.login(user, passwd) + #Switch to secure data connection. + self.prot_p() + + def ntransfercmd(self, cmd, rest=None): + conn, size = ftplib.FTP.ntransfercmd(self, cmd, rest) + if self._prot_p: + conn = self.context.wrap_socket(conn, + server_hostname=self.host, + session=self.sock.session) # reuse ssl session + return conn, size -def FtpWriteFile( host, port, user = 'anonymous', passwd = 'anonymous@', timeout = 30, serverPath = '.', fileName = '', file = None, useSftp = False): - if useSftp: +def FtpWriteFile( host, port, user = 'anonymous', passwd = 'anonymous@', timeout = 30, serverPath = '.', fileName = '', file = None, protocol='FTP'): + if protocol == 'SFTP': with CallCloseOnExit(paramiko.SSHClient()) as ssh: ssh.set_missing_host_key_policy( paramiko.AutoAddPolicy() ) ssh.load_system_host_keys() @@ -48,10 +67,20 @@ def FtpWriteFile( host, port, user = 'anonymous', passwd = 'anonymous@', timeout ) if fileOpened: file.close() - else: - ftp = ftplib.FTP() - ftp.connect( host, port, timeout = timeout ) - ftp.login( user, passwd ) + elif protocol == 'FTPS': + ftps = FtpsWithPort( host, user, passwd, port, timeout) + if serverPath and serverPath != '.': + ftps.cwd( serverPath ) + fileOpened = False + if file is None: + file = open(fileName, 'rb') + fileOpened = True + ftps.storbinary( 'STOR {}'.format(os.path.basename(fileName)), file ) + ftps.quit() + if fileOpened: + file.close() + else: #default to unencrypted FTP + ftp = FtpWithPort( host, user, passwd, port, timeout) if serverPath and serverPath != '.': ftp.cwd( serverPath ) fileOpened = False @@ -77,7 +106,7 @@ def FtpWriteHtml( html_in, team = False ): user = getattr( model, 'ftpUser', '' ) passwd = getattr( model, 'ftpPassword', '' ) serverPath = getattr( model, 'ftpPath', '' ) - useSftp = getattr( model, 'useSftp', False ) + protocol = getattr( model, 'ftpProtocol', 'FTP' ) with open( os.path.join(defaultPath, fileName), 'rb') as file: try: @@ -88,7 +117,7 @@ def FtpWriteHtml( html_in, team = False ): serverPath = serverPath, fileName = fileName, file = file, - useSftp = useSftp) + protocol = protocol) except Exception as e: Utils.writeLog( 'FtpWriteHtml Error: {}'.format(e) ) return e @@ -98,8 +127,8 @@ def FtpWriteHtml( html_in, team = False ): #------------------------------------------------------------------------------------------------ class FtpPublishDialog( wx.Dialog ): - fields = ['ftpHost', 'ftpPort', 'ftpPath', 'ftpUser', 'ftpPassword', 'urlPath', 'useSftp'] - defaults = ['', 21, '', 'anonymous', 'anonymous@', 'http://', False] + fields = ['ftpHost', 'ftpPort', 'ftpPath', 'ftpUser', 'ftpPassword', 'urlPath'] + defaults = ['', 21, '', 'anonymous', 'anonymous@', 'http://'] team = False def __init__( self, parent, html, team = False, id = wx.ID_ANY ): @@ -108,10 +137,13 @@ def __init__( self, parent, html, team = False, id = wx.ID_ANY ): self.html = html self.team = team + self.protocol = 'FTP' + bs = wx.GridBagSizer(vgap=0, hgap=4) - self.useFtp = wx.RadioButton( self, label=_("FTP"), style = wx.RB_GROUP ) - self.useSftp = wx.RadioButton( self, label=_("SFTP (SSH)") ) + self.useFtp = wx.RadioButton( self, label=_("FTP (unencrypted)"), style = wx.RB_GROUP ) + self.useFtps = wx.RadioButton( self, label=_("FTPS (FTP with TLS)") ) + self.useSftp = wx.RadioButton( self, label=_("SFTP (SSH file transfer)") ) self.Bind( wx.EVT_RADIOBUTTON,self.onSelectProtocol ) self.ftpHost = wx.TextCtrl( self, size=(256,-1), style=wx.TE_PROCESS_ENTER, value='' ) self.ftpPort = wx.lib.intctrl.IntCtrl( self, size=(256,-1), style=wx.TE_PROCESS_ENTER ) @@ -133,6 +165,10 @@ def __init__( self, parent, html, team = False, id = wx.ID_ANY ): row += 1 + bs.Add( self.useFtps, pos=(row,1), span=(1,1), border = border, flag=wx.RIGHT|wx.TOP|wx.ALIGN_LEFT ) + + row += 1 + bs.Add( self.useSftp, pos=(row,1), span=(1,1), border = border, flag=wx.RIGHT|wx.TOP|wx.ALIGN_LEFT ) row += 1 @@ -187,10 +223,13 @@ def __init__( self, parent, html, team = False, id = wx.ID_ANY ): def onSelectProtocol( self, event ): if self.useSftp.GetValue(): - self.useFtp.SetValue(False) + self.protocol = 'SFTP' self.ftpPort.SetValue(22) + elif self.useFtps.GetValue(): + self.protocol = 'FTPS' + self.ftpPort.SetValue(21) else: - self.useFtp.SetValue(True) + self.protocol = 'FTP' self.ftpPort.SetValue(21) def urlPathChanged( self, event = None ): @@ -213,6 +252,7 @@ def refresh( self ): else: for f, v in zip(FtpPublishDialog.fields, FtpPublishDialog.defaults): getattr(self, f).SetValue( getattr(model, f, v) ) + self.protocol = getattr(model, 'ftpProtocol', '') self.urlPathChanged() def setModelAttr( self ): @@ -220,9 +260,12 @@ def setModelAttr( self ): model = SeriesModel.model for f in FtpPublishDialog.fields: value = getattr(self, f).GetValue() - if getattr(model, f, None) != value: + if getattr( model, f, None ) != value: setattr( model, f, value ) model.setChanged() + if getattr( model, 'ftpProtocol', None ) != self.protocol: + setattr( model, 'ftpProtocol', self.protocol) + model.setChanged() model.urlFull = self.urlFull.GetLabel() def onOK( self, event ): From d65d90a58c995cee679877991253279fbefdc37c Mon Sep 17 00:00:00 2001 From: Kim Wall <30846798+kimble4@users.noreply.github.com> Date: Tue, 27 Dec 2022 22:35:14 +0000 Subject: [PATCH 10/13] Add files via upload --- SeriesMgr/SeriesModel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SeriesMgr/SeriesModel.py b/SeriesMgr/SeriesModel.py index 7c8e98341..2a3a20c47 100644 --- a/SeriesMgr/SeriesModel.py +++ b/SeriesMgr/SeriesModel.py @@ -247,8 +247,8 @@ class SeriesModel: ftpPath = '' ftpUser = '' ftpPassword = '' + ftpProtocol = '' urlPath = '' - useSftp = False @property def scoreByPoints( self ): From b5067c83960fab4d0b6210d09f414b3353c62617 Mon Sep 17 00:00:00 2001 From: kimble4 Date: Wed, 5 Apr 2023 23:16:13 +0100 Subject: [PATCH 11/13] Handle non-fatal EOF error, update GUI to reflect chosen protocol on refresh --- SeriesMgr/FtpWriteFile.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/SeriesMgr/FtpWriteFile.py b/SeriesMgr/FtpWriteFile.py index c60f0a693..92458433e 100644 --- a/SeriesMgr/FtpWriteFile.py +++ b/SeriesMgr/FtpWriteFile.py @@ -34,7 +34,7 @@ def __init__(self, host, user, passwd, port, timeout): class FtpsWithPort(ftplib.FTP_TLS): def __init__(self, host, user, passwd, port, timeout): ftplib.FTP_TLS.__init__(self) - #self.set_debuglevel(2) + #self.set_debuglevel(5) self.connect(host, port, timeout) self.auth() self.login(user, passwd) @@ -75,7 +75,13 @@ def FtpWriteFile( host, port, user = 'anonymous', passwd = 'anonymous@', timeout if file is None: file = open(fileName, 'rb') fileOpened = True - ftps.storbinary( 'STOR {}'.format(os.path.basename(fileName)), file ) + try: + ftps.storbinary( 'STOR {}'.format(os.path.basename(fileName)), file ) + except ftplib.all_errors as e: + if 'EOF occurred in violation of protocol' in str(e): + Utils.writeLog( 'FtpWriteFile ignored \"' + str(e) + '\" as this can be non-fatal. Check if the file exists on the server.') + else: + raise e ftps.quit() if fileOpened: file.close() @@ -250,9 +256,16 @@ def refresh( self ): for f, v in zip(FtpPublishDialog.fields, FtpPublishDialog.defaults): getattr(self, f).SetValue( v ) else: + self.protocol = getattr(model, 'ftpProtocol', '') + if self.protocol == 'SFTP': + self.useSftp.SetValue(True) + elif self.protocol == 'FTPS': + self.useFtps.SetValue(True) + else: + self.useFtp.SetValue(True) for f, v in zip(FtpPublishDialog.fields, FtpPublishDialog.defaults): getattr(self, f).SetValue( getattr(model, f, v) ) - self.protocol = getattr(model, 'ftpProtocol', '') + self.urlPathChanged() def setModelAttr( self ): @@ -263,9 +276,8 @@ def setModelAttr( self ): if getattr( model, f, None ) != value: setattr( model, f, value ) model.setChanged() - if getattr( model, 'ftpProtocol', None ) != self.protocol: - setattr( model, 'ftpProtocol', self.protocol) - model.setChanged() + setattr( model, 'ftpProtocol', self.protocol) + model.setChanged() model.urlFull = self.urlFull.GetLabel() def onOK( self, event ): From 3ab84a767ae8bc2d0fce23d0185196cecf43d708 Mon Sep 17 00:00:00 2001 From: kimble4 Date: Tue, 11 Apr 2023 17:53:42 +0100 Subject: [PATCH 12/13] Fix refresh state of FTP protocol radio button --- FtpWriteFile.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/FtpWriteFile.py b/FtpWriteFile.py index 2a5c554df..366f6d196 100644 --- a/FtpWriteFile.py +++ b/FtpWriteFile.py @@ -596,9 +596,17 @@ def refresh( self ): for f, v in zip(ftpFields, ftpDefaults): getattr(self, f).SetValue( v ) else: + self.protocol = getattr(race, 'ftpProtocol', '') + if self.protocol == 'SFTP': + self.useSftp.SetValue(True) + elif self.protocol == 'FTPS': + self.useFtps.SetValue(True) + else: + self.useFtp.SetValue(True) for f, v in zip(ftpFields, ftpDefaults): getattr(self, f).SetValue( getattr(race, f, v) ) - self.protocol = getattr(race, 'ftpProtocol', '') + + self.urlPathChanged() self.ftpUploadPhotosChanged() From 45f0e27f14fbddec561050481ebd498f6efdc49f Mon Sep 17 00:00:00 2001 From: kimble4 Date: Thu, 13 Apr 2023 17:24:56 +0100 Subject: [PATCH 13/13] Fix URL after publish. Catch EOF error. --- FtpWriteFile.py | 16 +++++++++++----- Properties.py | 36 ++++++++++++++++++------------------ 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/FtpWriteFile.py b/FtpWriteFile.py index 366f6d196..989baac84 100644 --- a/FtpWriteFile.py +++ b/FtpWriteFile.py @@ -132,11 +132,17 @@ def FtpWriteFile( host, port, user='anonymous', passwd='anonymous@', timeout=30, with ftputil.FTPHost(host, user, passwd, port, timeout, session_factory=FtpsWithPort) as ftp_host: ftp_host.makedirs( serverPath, exist_ok=True ) for i, f in enumerate(fname): - ftp_host.upload_if_newer( - f, - serverPath + '/' + os.path.basename(f), - (lambda byteStr, fname=f, i=i: callback(byteStr, fname, i)) if callback else None - ) + try: + ftp_host.upload_if_newer( + f, + serverPath + '/' + os.path.basename(f), + (lambda byteStr, fname=f, i=i: callback(byteStr, fname, i)) if callback else None + ) + except ftplib.all_errors as e: + if 'EOF occurred in violation of protocol' in str(e): + Utils.writeLog( 'FtpWriteFile ignored \"' + str(e) + '\" as this can be non-fatal. Check if the file exists on the server.') + else: + raise e ftp_host.close() else: #default to unencrypted FTP diff --git a/Properties.py b/Properties.py index ea5c4a11f..26b61f8d9 100644 --- a/Properties.py +++ b/Properties.py @@ -818,10 +818,10 @@ def commit( self ): #------------------------------------------------------------------------------------------------ class BatchPublishProperties( wx.Panel ): - def __init__( self, parent, id=wx.ID_ANY, testCallback=None, ftpCallback=None ): + def __init__( self, parent, id=wx.ID_ANY, publishCallback=None, ftpCallback=None ): super().__init__( parent, id ) - self.testCallback = testCallback + self.publishCallback = publishCallback self.ftpCallback = ftpCallback if ftpCallback: @@ -862,10 +862,10 @@ def __init__( self, parent, id=wx.ID_ANY, testCallback=None, ftpCallback=None ): else: fgs.AddSpacer( 0 ) - testBtn = wx.Button( self, label=_('Publish') ) - testBtn.Bind( wx.EVT_BUTTON, lambda event, iAttr=i: self.onTest(iAttr) ) - fgs.Add( testBtn, flag=wx.LEFT|wx.ALIGN_CENTRE_VERTICAL, border=8 ) - self.widget.append( (attrCB, ftpCB, testBtn) ) + publishBtn = wx.Button( self, label=_('Publish') ) + publishBtn.Bind( wx.EVT_BUTTON, lambda event, iAttr=i: self.onPublish(iAttr) ) + fgs.Add( publishBtn, flag=wx.LEFT|wx.ALIGN_CENTRE_VERTICAL, border=8 ) + self.widget.append( (attrCB, ftpCB, publishBtn) ) self.bikeRegChoice = wx.RadioBox( self, @@ -902,11 +902,11 @@ def __init__( self, parent, id=wx.ID_ANY, testCallback=None, ftpCallback=None ): self.SetSizer( vs ) - def onTest( self, iAttr ): - if self.testCallback: - self.testCallback() + def onPublish( self, iAttr ): + if self.publishCallback: + self.publishCallback() - attrCB, ftpCB, testBtn = self.widget[iAttr] + attrCB, ftpCB, publishBtn = self.widget[iAttr] doFtp = ftpCB and ftpCB.GetValue() doBatchPublish( iAttr, silent=False ) @@ -917,7 +917,7 @@ def onTest( self, iAttr ): if attr.filecode: fname = mainWin.getFormatFilename(attr.filecode) if doFtp and race.urlFull and race.urlFull != 'http://': - webbrowser.open( os.path.basename(race.urlFull) + '/' + os.path.basename(fname), new = 0, autoraise = True ) + webbrowser.open( race.urlFull, new = 0, autoraise = True ) else: Utils.LaunchApplication( fname ) else: @@ -927,32 +927,32 @@ def onTest( self, iAttr ): return def onSelect( self, iAttr ): - attrCB, ftpCB, testBtn = self.widget[iAttr] + attrCB, ftpCB, publishBtn = self.widget[iAttr] v = attrCB.GetValue() if ftpCB: ftpCB.Enable( v ) if not v: ftpCB.SetValue( False ) - testBtn.Enable( v ) + publishBtn.Enable( v ) def refresh( self ): race = Model.race for i, attr in enumerate(batchPublishAttr): raceAttr = batchPublishRaceAttr[i] - attrCB, ftpCB, testBtn = self.widget[i] + attrCB, ftpCB, publishBtn = self.widget[i] v = getattr( race, raceAttr, 0 ) if v & 1: attrCB.SetValue( True ) if ftpCB: ftpCB.Enable( True ) ftpCB.SetValue( v & 2 != 0 ) - testBtn.Enable( True ) + publishBtn.Enable( True ) else: attrCB.SetValue( False ) if ftpCB: ftpCB.SetValue( False ) ftpCB.Enable( False ) - testBtn.Enable( False ) + publishBtn.Enable( False ) self.bikeRegChoice.SetSelection( getattr(race, 'publishFormatBikeReg', 0) ) self.postPublishCmd.SetValue( race.postPublishCmd ) @@ -960,7 +960,7 @@ def commit( self ): race = Model.race for i, attr in enumerate(batchPublishAttr): raceAttr = batchPublishRaceAttr[i] - attrCB, ftpCB, testBtn = self.widget[i] + attrCB, ftpCB, publishBtn = self.widget[i] setattr( race, raceAttr, 0 if not attrCB.GetValue() else (1 + (2 if ftpCB and ftpCB.GetValue() else 0)) ) race.publishFormatBikeReg = self.bikeRegChoice.GetSelection() race.postPublishCmd = self.postPublishCmd.GetValue().strip() @@ -1076,7 +1076,7 @@ def __init__( self, parent, id=wx.ID_ANY ): super().__init__( parent, id, _("Batch Publish Results"), style=wx.DEFAULT_DIALOG_STYLE|wx.TAB_TRAVERSAL ) - self.batchPublishProperties = BatchPublishProperties(self, testCallback=self.commit, ftpCallback=self.onToggleFtp) + self.batchPublishProperties = BatchPublishProperties(self, publishCallback=self.commit, ftpCallback=self.onToggleFtp) self.batchPublishProperties.refresh() self.ftp = FtpProperties( self, uploadNowButton=False )