diff --git a/FtpWriteFile.py b/FtpWriteFile.py index 553ef79bf..989baac84 100644 --- a/FtpWriteFile.py +++ b/FtpWriteFile.py @@ -4,7 +4,9 @@ import os import sys import webbrowser +import ftplib import ftputil +import ftputil.session import paramiko from urllib.parse import quote import datetime @@ -21,10 +23,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() @@ -55,8 +57,35 @@ 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, 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] @@ -64,7 +93,7 @@ def FtpWriteFile( host, user='anonymous', passwd='anonymous@', timeout=30, serve # 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('\\') @@ -85,22 +114,39 @@ def FtpWriteFile( host, user='anonymous', passwd='anonymous@', timeout=30, serve 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() - ssh.connect( host, sftpPort, username, passwd ) + ssh.connect( host, port, 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 ) - else: - with ftputil.FTPHost( host, user, passwd ) 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): + 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 + 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( @@ -108,6 +154,7 @@ def FtpWriteFile( host, user='anonymous', passwd='anonymous@', timeout=30, serve 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: @@ -126,10 +173,11 @@ 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', ''), - 'useSftp': getattr(race, 'useSftp', False), + 'protocol': getattr(race, 'ftpProtocol', 'FTP'), 'fname': fname or [], 'callback': callback, } @@ -350,8 +398,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', 'ftpUploadDuringRace', 'urlPath', 'ftpUploadPhotos'] +ftpDefaults = ['', 21, '', '', 'anonymous', 'anonymous@', False, 'http://', False] def GetFtpPublish( isDialog=True ): ParentClass = wx.Dialog if isDialog else wx.Panel @@ -364,11 +412,17 @@ 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.useSftp = wx.CheckBox( self, label=_("Use SFTP Protocol (on port 22)") ) + 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 ) 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 +447,20 @@ 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.useFtps, 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 +521,17 @@ 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.protocol = 'SFTP' + self.ftpPort.SetValue(22) + elif self.useFtps.GetValue(): + self.protocol = 'FTPS' + self.ftpPort.SetValue(21) + else: + self.protocol = 'FTP' + self.ftpPort.SetValue(21) def onFtpTest( self, event ): self.commit() @@ -529,8 +602,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.urlPathChanged() self.ftpUploadPhotosChanged() @@ -540,6 +622,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() diff --git a/Properties.py b/Properties.py index aea8a5cd8..26b61f8d9 100644 --- a/Properties.py +++ b/Properties.py @@ -818,20 +818,20 @@ 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: - 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 ) @@ -839,7 +839,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 ) @@ -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 ) @@ -1273,7 +1273,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') ), diff --git a/SeriesMgr/FtpWriteFile.py b/SeriesMgr/FtpWriteFile.py index a592bdbbf..92458433e 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,43 +14,116 @@ 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() + +class FtpWithPort(ftplib.FTP): + 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(5) + 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, protocol='FTP'): + if protocol == 'SFTP': + with CallCloseOnExit(paramiko.SSHClient()) as ssh: + ssh.set_missing_host_key_policy( paramiko.AutoAddPolicy() ) + ssh.load_system_host_keys() + ssh.connect( host, port, 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() + 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 + 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() + else: #default to unencrypted FTP + ftp = FtpWithPort( host, user, passwd, port, timeout) + 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 ) model = SeriesModel.model host = getattr( model, 'ftpHost', '' ) + port = getattr( model, 'ftpPort', 21 ) user = getattr( model, 'ftpUser', '' ) passwd = getattr( model, 'ftpPassword', '' ) serverPath = getattr( model, 'ftpPath', '' ) + protocol = getattr( model, 'ftpProtocol', 'FTP' ) with open( os.path.join(defaultPath, fileName), 'rb') as file: try: FtpWriteFile( host = host, + port = port, user = user, passwd = passwd, serverPath = serverPath, fileName = fileName, - file = file ) + file = file, + protocol = protocol) except Exception as e: Utils.writeLog( 'FtpWriteHtml Error: {}'.format(e) ) return e @@ -59,17 +133,26 @@ def FtpWriteHtml( html_in ): #------------------------------------------------------------------------------------------------ class FtpPublishDialog( wx.Dialog ): - fields = ['ftpHost', 'ftpPath', 'ftpUser', 'ftpPassword', 'urlPath'] - defaults = ['', '', 'anonymous', 'anonymous@' 'http://'] + fields = ['ftpHost', 'ftpPort', 'ftpPath', 'ftpUser', 'ftpPassword', 'urlPath'] + defaults = ['', 21, '', 'anonymous', 'anonymous@', 'http://'] + team = False - def __init__( self, parent, html, id = wx.ID_ANY ): - super().__init__( parent, id, "Ftp Publish Results", + def __init__( self, parent, html, team = False, id = wx.ID_ANY ): + super().__init__( parent, id, "(S)FTP Publish Results", style=wx.DEFAULT_DIALOG_STYLE|wx.TAB_TRAVERSAL ) self.html = html + self.team = team + self.protocol = 'FTP' + bs = wx.GridBagSizer(vgap=0, hgap=4) + 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 ) 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='' ) @@ -81,10 +164,32 @@ def __init__( self, parent, html, id = wx.ID_ANY ): row = 0 border = 8 - bs.Add( wx.StaticText( self, label=_("Ftp Host Name:")), pos=(row,0), span=(1,1), border = border, + + 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.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 + + 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 ) @@ -122,6 +227,17 @@ def __init__( self, parent, html, id = wx.ID_ANY ): self.CentreOnParent(wx.BOTH) self.SetFocus() + def onSelectProtocol( self, event ): + if self.useSftp.GetValue(): + self.protocol = 'SFTP' + self.ftpPort.SetValue(22) + elif self.useFtps.GetValue(): + self.protocol = 'FTPS' + self.ftpPort.SetValue(21) + else: + self.protocol = 'FTP' + self.ftpPort.SetValue(21) + def urlPathChanged( self, event = None ): url = self.urlPath.GetValue() fileName = Utils.getFileName() @@ -130,7 +246,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 ) @@ -140,8 +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.urlPathChanged() def setModelAttr( self ): @@ -149,14 +273,16 @@ 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() + setattr( model, 'ftpProtocol', self.protocol) + model.setChanged() model.urlFull = self.urlFull.GetLabel() 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/Results.py b/SeriesMgr/Results.py index 0cd584866..e691128f0 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 1c62da55c..f8ae57b13 100644 --- a/SeriesMgr/SeriesModel.py +++ b/SeriesMgr/SeriesModel.py @@ -250,9 +250,11 @@ class SeriesModel: aliasTeamLookup = {} ftpHost = '' + ftpPort = 21 ftpPath = '' ftpUser = '' ftpPassword = '' + ftpProtocol = '' urlPath = '' @property diff --git a/SeriesMgr/TeamResults.py b/SeriesMgr/TeamResults.py index d4adf426a..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 ) @@ -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 ) 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. diff --git a/helptxt/Properties.md b/helptxt/Properties.md index 53eb30b51..6fb3647f1 100644 --- a/helptxt/Properties.md +++ b/helptxt/Properties.md @@ -295,8 +295,9 @@ 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). +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. User|FTP/SFTP User name Password|FTP/SFTP Password