FTP mit Java

+ andere TechDocs
+ SFTP
+ FTP
+ FTPClient
+ FtpServer
+


FTP (File Transfer Protocol) ist ein universelles IP-basierendes Protokoll zur Übertragung von Dateien.

Die im Folgenden gezeigten Beispiele demonstrieren:



Inhalt

  1. FTP versus FTPS versus SFTP versus SSH
  2. Rudimentäres FTP-Beispiel
  3. FTP-Upload und -Download mit Java
  4. FTP mit FileZilla, cURL, Webbrowser und Windows-Explorer, sowie FTP-Server als Netzlaufwerk verbinden
  5. Problem wegen nur teilweise kopierter Dateien
  6. Probleme bei vielen FTP-Connections
  7. Probleme wegen Active Mode statt Passive Mode
  8. Probleme mit einem FTP-Server im passiven Modus
  9. Probleme beim Unzip
  10. Probleme beim Append
  11. Probleme wegen "Connection closed"
  12. Wrapper für FTP-Server / Fileserver
  13. Stand-alone-FTP-Server


FTP versus FTPS versus SFTP versus SSH

FTP (File Transfer Protocol)

FTP ist ein Protokollstandard zur Übertragung von Dateien über TCP/IP-Netzwerke, standardisiert im RFC 959 von 1985. Es verwendet die Ports 20 (Data Channel) und 21 (Command Channel) sowie weitere temporäre Ports. Benutzernamen, Passwörter und Daten werden ohne weitere Maßnahmen unverschlüsselt übertragen.

FTPS (FTP über SSL/TLS)

FTPS erweitert das Netzwerkdateiübertragungsprotokoll FTP um einen Sicherheitslayer per SSL/TLS, standardisiert im RFC 4217. Die Authentifizierung wird verschlüsselt. Die Übertragung der Nutzdaten kann verschlüsselt oder unverschlüsselt erfolgen.

SFTP (SSH File Transfer Protocol)

SFTP ist ebenfalls ein Protokollstandard zur Übertragung von Dateien über TCP/IP-Netzwerke, standardisiert im RFC 4253. Aber anders als FTPS erweitert SFTP nicht das FTP-Protokoll, sondern stellt ein eigenständiges Protokoll dar. Es basiert auf SSH (Secure Shell) und verwendet verschlüsselte Authentifizierung und verschlüsselte Datenübertragung. Anders als FTP und FTPS benötigt SFTP nur einen Port, normalerweise Port 22 (wie SSH).

Java-SFTP-Programmierbeispiele finden Sie unter java-sftp.htm.

SSH (Secure Shell)

SSH bezeichnet sowohl ein Netzwerkdatenübertragungsprotokoll als auch ein dieses Protokoll verwendendes Programm, mit dem Kommandos über eine TCP/IP-Netzwerkverbindung auf einem entfernten Rechner ausgeführt werden können (Fernwartung). SSH ermöglicht eine sichere, authentifizierte und verschlüsselte Verbindung zwischen zwei Rechnern über ein unsicheres Netzwerk. SSH verwendet den Port 22.



Rudimentäres FTP-Beispiel

Ein sehr einfaches direktes Programmierbeispiel für einen FTP-Download per URL.openStream() finden Sie unter java-net.htm#FileFromUrl.



FTP-Upload und -Download mit Java

Das folgende Beispiel zeigt die Programmierung des FTP-Upload und -Download mit Hilfe der FTPClient-Klasse aus dem Apache-Commons-Net-Projekt. Das Besondere dabei ist der JUnit-Test des FTP-Zugriffs mit einem temporären embedded FTP-Server mit Hilfe der FtpServer-Klasse aus dem Apache-FtpServer-Projekt.

Folgendermaßen führen Sie das Beispiel aus:

  1. Das Java SE JDK und Maven müssen installiert sein.

  2. Öffnen Sie ein Kommandozeilenfenster, wechseln Sie in Ihr Projekte-Verzeichnis (z.B. D:\MeinWorkspace) und erzeugen Sie eine neue Projektstruktur:

    cd \MeinWorkspace

    md FtpUploadDownload

    cd FtpUploadDownload

    md src\main\java\de\meinefirma\meinprojekt\ftp

    md src\test\java\de\meinefirma\meinprojekt\ftp

  3. Erstellen Sie im FtpUploadDownload-Projektverzeichnis die Projektkonfiguration: pom.xml

    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
      <modelVersion>4.0.0</modelVersion>
      <groupId>de.meinefirma.meinprojekt</groupId>
      <artifactId>FtpUploadDownload</artifactId>
      <version>1.0-SNAPSHOT</version>
      <packaging>jar</packaging>
      <name>FtpUploadDownload</name>
      <dependencies>
        <dependency>
          <groupId>commons-net</groupId>
          <artifactId>commons-net</artifactId>
          <version>3.3</version>
        </dependency>
        <dependency>
          <groupId>org.apache.ftpserver</groupId>
          <artifactId>ftpserver-core</artifactId>
          <version>1.0.6</version>
          <scope>test</scope>
        </dependency>
        <dependency>
          <groupId>org.slf4j</groupId>
          <artifactId>slf4j-api</artifactId>
          <version>1.7.12</version>
          <scope>test</scope>
        </dependency>
        <dependency>
          <groupId>org.slf4j</groupId>
          <artifactId>slf4j-log4j12</artifactId>
          <version>1.7.12</version>
          <scope>test</scope>
        </dependency>
        <dependency>
          <groupId>junit</groupId>
          <artifactId>junit</artifactId>
          <version>4.12</version>
          <scope>test</scope>
        </dependency>
      </dependencies>
    </project>
    
  4. Erstellen Sie im FtpUploadDownload-Projektverzeichnis eine Testtextdatei: TestSrc.txt

    Mein Text.
    
  5. Erstellen Sie im src\main\java\de\meinefirma\meinprojekt\ftp-Verzeichnis die Klasse: FtpUploadDownloadUtil.java

    package de.meinefirma.meinprojekt.ftp;
    
    import java.io.*;
    import org.apache.commons.net.ftp.FTPClient;
    
    /**
     * FTP-Utility, basierend auf Apache FTPClient:
     * {@link "http://commons.apache.org/net/apidocs/org/apache/commons/net/ftp/FTPClient.html"}
    */
    public class FtpUploadDownloadUtil
    {
       /**
        * FTP-Dateienliste.
        * @return String-Array der Dateinamen auf dem FTP-Server
        */
       public static String[] list( String host, int port, String usr, String pwd ) throws IOException
       {
          FTPClient ftpClient = new FTPClient();
          String[]  filenameList;
    
          try {
             ftpClient.connect( host, port );
             ftpClient.login( usr, pwd );
             filenameList = ftpClient.listNames();
             ftpClient.logout();
          } finally {
             ftpClient.disconnect();
          }
    
          return filenameList;
       }
    
       /**
        * FTP-Client-Download.
        * @return true falls ok
        */
       public static boolean download( String localResultFile, String remoteSourceFile,
             String host, int port, String usr, String pwd, boolean showMessages ) throws IOException
       {
          FTPClient        ftpClient = new FTPClient();
          FileOutputStream fos = null;
          boolean          resultOk = true;
    
          try {
             ftpClient.connect( host, port );
             if( showMessages ) { System.out.println( ftpClient.getReplyString() ); }
             resultOk &= ftpClient.login( usr, pwd );
             if( showMessages ) { System.out.println( ftpClient.getReplyString() ); }
             fos = new FileOutputStream( localResultFile );
             resultOk &= ftpClient.retrieveFile( remoteSourceFile, fos );
             if( showMessages ) { System.out.println( ftpClient.getReplyString() ); }
             resultOk &= ftpClient.logout();
             if( showMessages ) { System.out.println( ftpClient.getReplyString() ); }
          } finally {
             try { if( fos != null ) { fos.close(); } } catch( IOException e ) {/* nothing to do */}
             ftpClient.disconnect();
          }
    
          return resultOk;
       }
    
       /**
        * FTP-Client-Upload.
        * @return true falls ok
        */
       public static boolean upload( String localSourceFile, String remoteResultFile,
             String host, int port, String usr, String pwd, boolean showMessages ) throws IOException
       {
          FTPClient       ftpClient = new FTPClient();
          FileInputStream fis = null;
          boolean         resultOk = true;
    
          try {
             ftpClient.connect( host, port );
             if( showMessages ) { System.out.println( ftpClient.getReplyString() ); }
             resultOk &= ftpClient.login( usr, pwd );
             if( showMessages ) { System.out.println( ftpClient.getReplyString() ); }
             fis = new FileInputStream( localSourceFile );
             resultOk &= ftpClient.storeFile( remoteResultFile, fis );
             if( showMessages ) { System.out.println( ftpClient.getReplyString() ); }
             resultOk &= ftpClient.logout();
             if( showMessages ) { System.out.println( ftpClient.getReplyString() ); }
          } finally {
             try { if( fis != null ) { fis.close(); } } catch( IOException e ) {/* nothing to do */}
             ftpClient.disconnect();
          }
    
          return resultOk;
       }
    }
    
  6. Erstellen Sie im src\test\java\de\meinefirma\meinprojekt\ftp-Verzeichnis die Klasse: FtpTestUtil.java

    package de.meinefirma.meinprojekt.ftp;
    
    import java.io.*;
    import java.util.*;
    import org.apache.ftpserver.*;
    import org.apache.ftpserver.ftplet.*;
    import org.apache.ftpserver.listener.ListenerFactory;
    import org.apache.ftpserver.usermanager.*;
    import org.apache.ftpserver.usermanager.impl.*;
    
    /**
     * FTP-Test-Utility, basierend auf Apache FtpServer:
     * {@link "http://www.jarvana.com/jarvana/view/org/apache/ftpserver/ftpserver-core/1.0.6/ftpserver-core-1.0.6-javadoc.jar!/org/apache/ftpserver/FtpServer.html"}
     */
    public class FtpTestUtil
    {
       /**
        * Erzeuge FTP-Server.
        * @param ftpPort           FTP-Port, z.B. 2121
        * @param ftpHomeDir        FTP-Verzeichnis, z.B. "target/FtpHome"
        * @param readUserName      leseberechtigter Benutzer: Name
        * @param readUserPwd       leseberechtigter Benutzer: Passwort
        * @param writeUserName     schreibberechtigter Benutzer: Name
        * @param writeUserPwd      schreibberechtigter Benutzer: Passwort
        * @param ftpUsersPropsFile kann null sein, oder z.B. "target/FtpUsers.properties"
        * @param maxLogins         maximale Anzahl von Logins (0 fuer Defaultwert)
        */
       public static FtpServer createFtpServer( int ftpPort, String ftpHomeDir,
             String readUserName, String readUserPwd, String writeUserName, String writeUserPwd ) throws FtpException, IOException
       {
          return createFtpServer( ftpPort, ftpHomeDir, readUserName, readUserPwd, writeUserName, writeUserPwd, null, 0 );
       }
    
       public static FtpServer createFtpServer( int ftpPort, String ftpHomeDir,
             String readUserName, String readUserPwd, String writeUserName, String writeUserPwd,
             String ftpUsersPropsFile, int maxLogins ) throws FtpException, IOException
       {
          return createFtpServer( ftpPort, ftpHomeDir, readUserName, readUserPwd, writeUserName, writeUserPwd,
                                  ftpUsersPropsFile, maxLogins, 0 );
       }
    
       public static FtpServer createFtpServer( int ftpPort, String ftpHomeDir,
             String readUserName, String readUserPwd, String writeUserName, String writeUserPwd,
             String ftpUsersPropsFile, int maxLogins, int maxIdleTimeSec ) throws FtpException, IOException
       {
          File fhd = new File( ftpHomeDir );
          if( !fhd.exists() ) fhd.mkdirs();
    
          ListenerFactory listenerFactory = new ListenerFactory();
          listenerFactory.setPort( ftpPort );
    
          PropertiesUserManagerFactory userManagerFactory = new PropertiesUserManagerFactory();
          userManagerFactory.setPasswordEncryptor( new SaltedPasswordEncryptor() );
          if( ftpUsersPropsFile != null && ftpUsersPropsFile.trim().length() > 0 ) {
             File upf = new File( ftpUsersPropsFile );
             if( !upf.exists() ) upf.createNewFile();
             userManagerFactory.setFile( upf );
          }
    
          // Einen Nur-Lese-User und einen User mit Schreibberechtigung anlegen:
          UserManager userManager = userManagerFactory.createUserManager();
          BaseUser userRd = new BaseUser();
          BaseUser userWr = new BaseUser();
          userRd.setName( readUserName );
          userRd.setPassword( readUserPwd );
          userRd.setHomeDirectory( ftpHomeDir );
          userWr.setName( writeUserName );
          userWr.setPassword( writeUserPwd );
          userWr.setHomeDirectory( ftpHomeDir );
          if( maxIdleTimeSec > 0 ) {
             userRd.setMaxIdleTime( maxIdleTimeSec );
             userWr.setMaxIdleTime( maxIdleTimeSec );
          }
          List<Authority> authorities = new ArrayList<Authority>();
          authorities.add( new WritePermission() );
          userWr.setAuthorities( authorities );
          userManager.save( userRd );
          userManager.save( userWr );
    
          FtpServerFactory serverFactory = new FtpServerFactory();
          serverFactory.addListener( "default", listenerFactory.createListener() );
          serverFactory.setUserManager( userManager );
          if( maxLogins > 0 ) {
             ConnectionConfigFactory ccf = new ConnectionConfigFactory();
             ccf.setMaxLogins( maxLogins );
             serverFactory.setConnectionConfig( ccf.createConnectionConfig() );
          }
          return serverFactory.createServer();
       }
    }
    
  7. Erstellen Sie im src\test\java\de\meinefirma\meinprojekt\ftp-Verzeichnis die Klasse: FtpUploadDownloadUtilTest.java

    package de.meinefirma.meinprojekt.ftp;
    
    import java.io.*;
    import java.util.Arrays;
    import org.apache.ftpserver.FtpServer;
    import org.apache.ftpserver.ftplet.FtpException;
    import org.junit.*;
    
    public class FtpUploadDownloadUtilTest
    {
       private static final int    FTP_PORT           = 2121;
       private static final String FTP_HOST           = "localhost";
       private static final String FTP_HOME_DIR       = "target/FtpHome";
       private static final String FTPUSERSPROPS_FILE = "target/FtpUsers.properties";
       private static final String READ_USER_NAME     = "ReadUserName";
       private static final String READ_USER_PWD      = "ReadUserPwd";
       private static final String WRITE_USER_NAME    = "WriteUserName";
       private static final String WRITE_USER_PWD     = "WriteUserPwd";
       private static FtpServer ftpServer;
    
       @BeforeClass
       public static void startFtpServer() throws FtpException, IOException
       {
          ftpServer = FtpTestUtil.createFtpServer( FTP_PORT, FTP_HOME_DIR,
                READ_USER_NAME, READ_USER_PWD, WRITE_USER_NAME, WRITE_USER_PWD, FTPUSERSPROPS_FILE, 0 );
          ftpServer.start();
       }
    
       @AfterClass
       public static void stoppFtpServer()
       {
          // Um den FTP-Server von ausserhalb des Tests eine Zeit lang ueber
          // ftp://WriteUserName:WriteUserPwd@localhost:2121
          // anzusprechen, kann folgende Zeile aktiviert werden:
          // try { Thread.sleep( 55000 ); } catch( InterruptedException e ) {/*ok*/}
          ftpServer.stop();
          ftpServer = null;
       }
    
       @Test
       public void testFtp() throws IOException
       {
          final String LOCAL_SRC_FILE = "TestSrc.txt";
          final String LOCAL_DST_FILE = "target/TestDst.txt";
          final String REMOTE_FILE    = "Test.txt";
    
          File testFile = new File( LOCAL_SRC_FILE );
          if( !testFile.exists() ) testFile.createNewFile();
    
          // Upload-Versuch ohne Schreibberechtigung muss fehlschlagen:
          Assert.assertFalse( "READ_USER", FtpUploadDownloadUtil.upload( LOCAL_SRC_FILE, REMOTE_FILE, FTP_HOST, FTP_PORT,
                READ_USER_NAME, READ_USER_PWD, false ) );
    
          // Teste Upload mit Schreibberechtigung:
          Assert.assertTrue( "WRITE_USER", FtpUploadDownloadUtil.upload( LOCAL_SRC_FILE, REMOTE_FILE, FTP_HOST, FTP_PORT,
                WRITE_USER_NAME, WRITE_USER_PWD, false ) );
          Assert.assertTrue( "REMOTE_FILE.exists", new File( FTP_HOME_DIR, REMOTE_FILE ).exists() );
    
          // Teste Download:
          Assert.assertTrue( "Download", FtpUploadDownloadUtil.download( LOCAL_DST_FILE, REMOTE_FILE, FTP_HOST, FTP_PORT,
                READ_USER_NAME, READ_USER_PWD, false ) );
          Assert.assertTrue( "LOCAL_DST_FILE.exists", new File( LOCAL_DST_FILE ).exists() );
    
          // Teste Auflistung:
          String[] remoteFilenameList = FtpUploadDownloadUtil.list( FTP_HOST, FTP_PORT, READ_USER_NAME, READ_USER_PWD );
          Assert.assertTrue( "remoteFilenameList", Arrays.asList( remoteFilenameList ).contains( REMOTE_FILE ) );
       }
    }
    
  8. Führen Sie den Test aus:

    cd \MeinWorkspace\FtpUploadDownload

    mvn test

    tree /F

  9. Nach der Ausführung des Tests erhalten Sie folgende Verzeichnisse und Dateien:

    [\MeinWorkspace\FtpUploadDownload]
     |- [src]
     |   |- [main]
     |   |   '- [java]
     |   |       '- [de]
     |   |           '- [meinefirma]
     |   |               '- [meinprojekt]
     |   |                   '- [ftp]
     |   |                       '- FtpUploadDownloadUtil.java
     |   '- [test]
     |       '- [java]
     |           '- [de]
     |               '- [meinefirma]
     |                   '- [meinprojekt]
     |                       '- [ftp]
     |                           |- FtpTestUtil.java
     |                           '- FtpUploadDownloadUtilTest.java
     |- [target]
     |   |- [classes]
     |   |   '- ...
     |   |- [FtpHome] . . . . . . . . . . . . . [Home-Verzeichnis vom embedded FTP-Server]
     |   |   '- Test.txt  . . . . . . . . . . . [per FTP hochgeladene Datei (Upload)]
     |   |- [surefire-reports]
     |   |   '- ...
     |   |- [test-classes]
     |   |   '- ...
     |   |- FtpUsers.properties . . . . . . . . [User-Eigenschaften, falls 'ftpUsersPropsFile' an 'createFtpServer()' uebergeben wurde]
     |   '- TestDst.txt . . . . . . . . . . . . [per FTP heruntergeladene Datei (Download)]
     |- pom.xml
     '- TestSrc.txt
    
  10. Lassen Sie sich anzeigen, was in die FtpUsers.properties eingetragen wurde:

    type target\FtpUsers.properties | sort

    In dieser Datei sehen Sie nicht nur die während des Tests hinzugefügten Benutzer und deren Eigenschaften, sondern Sie hätten darin auch vorher weitere Benutzer definieren können, die während des Tests hätten verwendet werden können. Wenn Sie diese Datei nicht benötigen, können Sie der FtpUtils.createFtpServer()-Methode als ftpUsersPropsFile-Parameter "null" übergeben.



FTP mit FileZilla, cURL, Webbrowser und Windows-Explorer, sowie FTP-Server als Netzlaufwerk verbinden

  1. Entfernen Sie im obigen Programmierbeispiel vor "Thread.sleep( 55000 )" die Auskommentierung, starten Sie den Test neu, und rufen Sie (innerhalb der angegebenen 55 Sekunden) die hochgeladene Testtextdatei vom FTP-Server per FTP ab (nach dem "start cmd /C mvn test"-Kommando jeweils kurz warten):

    a) Mit komfortablen FTP-Client-Programmen wie zum Beispiel FileZilla.

    b) Mit cURL:

    cd \MeinWorkspace\FtpUploadDownload

    start cmd /C mvn test

    curl -l ftp://ReadUserName:ReadUserPwd@localhost:2121

    curl ftp://ReadUserName:ReadUserPwd@localhost:2121/Test.txt

    curl -T TestSrc.txt ftp://WriteUserName:WriteUserPwd@localhost:2121/Upload-per-curl-Test.txt

    curl ftp://ReadUserName:ReadUserPwd@localhost:2121/Upload-per-curl-Test.txt

    c) Mit dem Webbrowser:

    start cmd /C mvn test

    start ftp://WriteUserName:WriteUserPwd@localhost:2121

    Klicken Sie auf der FTP-Webseite auf den Test.txt-Link.

    d) Mit dem Windows-Explorer:

    start cmd /C mvn test

    explorer.exe ftp://WriteUserName:WriteUserPwd@localhost:2121

    Sie können im Windows-Explorer Dateien und ganze Verzeichnisbäume mit "Drag and Drop" per FTP kopieren.

  2. Folgendermaßen können Sie in Windows 7 über den Windows-Explorer ein Verzeichnis auf einem FTP-Server ähnlich wie ein Festplattenlaufwerk einbinden:
    Extras | Netzlaufwerk verbinden... | Verbindung mit einer Website herstellen, auf der Sie Dokumente und Bilder speichern können | Weiter | Weiter | URL eintragen (z.B. ftp://WriteUserName:WriteUserPwd@localhost:2121) | Weiter | Namen eintragen (z.B. Test-FTP) | Weiter.
    Falls dies nicht funktioniert: Überprüfen Sie die Aktivierung folgender Einstellung:
    Start | Systemsteuerung | Netzwerk und Internet | Internetoptionen | Reiter Erweitert | Rubrik Browsen | FTP-Ordneransicht aktivieren (außerhalb von Internet Explorer).

    Dies funktioniert auch in älteren Windows-Versionen, aber dann lauten einige Menüeinträge etwas anders.

  3. Sehen Sie sich die FTP-Kommandos und -Meldungen an (z.B. während des Testlaufs). Am einfachsten geht dies, wenn Sie in der pom.xml die Zeile "<artifactId>slf4j-log4j12</artifactId>" durch "<artifactId>slf4j-simple</artifactId>" ersetzen.
    Sehen Sie sich auch die Bedeutung der FTP-Kommandos an.

  4. Sie können den Apache FtpServer natürlich nicht nur "embedded", sondern auch "stand-alone" verwenden, siehe hierzu die beispielhafte Vorgehensweise unter Stand-alone-FTP-Server.

  5. Sehen Sie sich die Doku zum Apache FtpServer an.



Problem wegen nur teilweise kopierter Dateien

Wenn ein Prozess eine Datei zum FTP-Server uploadet und während des Uploads ein anderer Prozess dieselbe Datei downloadet, dann erhält der zweite Prozess nur einen Teil der Datei, ohne dass es zu einer Fehlermeldung kommt.

Sie können dieses Problem leicht nachvollziehen, indem Sie eine große Datei mit dem Namen "MeineGrosseTestdatei" erstellen (je nach Netzwerkgeschwindigkeit kann eine Datei mit 100 MByte genügen), folgende Kommandos in einer Batchdatei speichern und diese ausführen (passen Sie die FTP-Parameter und die URL an):

start curl -T MeineGrosseTestdatei ftp://WriteUserName:WriteUserPwd@meinftpserver:4221/MeineGrosseTestdatei-2
@ping -n 3 127.0.0.1 >nul
curl ftp://WriteUserName:WriteUserPwd@meinftpserver:4221/MeineGrosseTestdatei-2 -o MeineGrosseTestdatei-3
dir

Die downgeloadete MeineGrosseTestdatei-3 hat nur einen Bruchteil der Größe von der originalen MeineGrosseTestdatei.

Eine einfache Lösung zur Vermeidung dieses Problems könnte folgender Prozess sein:

  1. Sie verabreden mit Ihrem Kommunikationspartner, dass bestimmte Dateinamenpatterns nur für temporäre Dateien verwendet werden, zum Beispiel Dateien, die auf "$$" oder ".part" enden.
  2. Der Upload erfolgt mit einem temporären Dateinamen, welcher diesem Pattern entspricht.
  3. Erst wenn der Upload fertig ist, wird der Dateiname auf dem FTP-Server in den korrekten Dateinamen umgenannt.

Zusätzlich sollte vor einem Download die Größe der downzuloadenden Original-Datei auf dem Server ermittelt werden und nach dem Download sollte die Größe der Original-Datei auf dem Server erneut ermittelt werden sowie die Größe der downgeloadeten Datei. Die Dateigröße darf sich nicht verändert haben, alle drei Werte müssen identisch sein.



Probleme bei vielen FTP-Connections

Bei zu häufigen FTP-Logins und zu vielen FTP-Aktionen können Sie entweder eine Blockade oder folgende Exception erhalten:

java.net.BindException: Address already in use: connect

oder:

org.apache.commons.net.ftp.FTPConnectionClosedException: Connection closed without indication.

Falls der Apache FtpServer verwendet wird, dann steht in dessen ftpd.log-Logfile:

java.io.IOException: Eine vorhandene Verbindung wurde vom Remotehost geschlossen

Einer der möglichen Gründe ist, dass nur begrenzt viele temporär dynamisch zuordnungsbare Ports zur Verfügung stehen. Portnummern liegen im Bereich von 0 bis 65535 (dabei ist der Bereich bis 1024 für die "Well Known Ports" durch die IANA reserviert). Die verschiedenen Betriebssysteme nutzen aber nur einen Teil des Bereichs: Zum Beispiel Windows XP nutzt defaultmäßig nur den Bereich von 1025 bis 5000 (einstellbar in der Registry mit MaxUserPort unter HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters). Unter Windows 7 ist normalerweise ein größerer Bereich eingestellt, beispielsweise von 49152 bis 65535. Dies kann ermittelt werden über:

netsh interface ipv4 show dynamicportrange tcp

und eingestellt werden über:

netsh int ipv4 set dynamicport tcp start=49152 num=16384

Unter vielen Linux-Varianten können Sie den Bereich ermitteln mit:

cat /proc/sys/net/ipv4/ip_local_port_range

Unter allen Betriebssystemen zu beachten ist, dass benutzte Ports nach der Verwendung noch einige Zeit vom Betriebssystem reserviert bleiben, bei Windows normalerweise für 2 Minuten, beobachtbar über die Kommandos:

netstat -aon | findstr WARTEND

netstat -a | find /C "WARTEND"

Weiterhin ist zu beachten, dass pro FTP-Verbindung oft viele Ports benötigt werden.

Ein sehr übersichtliches Tool zur Beobachtung der verwendeten Ports unter Windows ist CurrPorts (cports) von NirSoft.

Weitere Gründe für Probleme bei vielen FTP-Connections können sein:

Falls Sie auf der Serverseite die Exception "java.net.SocketException: Too many files open" erhalten, gehen dem Betriebssystem die File-Handles aus, siehe hierzu: FAQ: My server fails with java.net.SocketException: Too many files open.
Wie Sie unter Linux mehr File-Handles ermöglichen, erfahren Sie unter File-Handles / ulimit und Increasing file descriptors limit.

Kontrollieren Sie die Konfiguration Ihres FTP-Servers.
Falls Sie den Apache FtpServer verwenden: Sehen Sie sich die Einträge zu max-threads und max-logins in der FTP-Config-XML-Datei und zu ftpserver.user...maxloginnumber in der FTP-Users-Properties-Datei an.
Falls Sie den vsftpd verwenden: Sehen Sie sich die Einträge in der vsftpd.conf an, zum Beispiel zu max_clients und max_per_ip (insbesondere falls Sie "421"-Fehler erhalten).
Beim vsftpd-FTP-Server können Sie Logging aktivieren durch Setzen von dual_log_enable, xferlog_enable und log_ftp_protocol auf YES. Beachten Sie auch xferlog_std_format.



Probleme wegen Active Mode statt Passive Mode

Falls Sie als Antwort (z.B. bei ftpClient.getReplyString() nach ftpClient.listNames()) erhalten:

500 PORT/EPRT (Active Mode/Extended Active Mode) is not supported. Use PASV/EPSV

Dann müssen Sie wahrscheinlich in den Passive Mode umschalten, z.B. so:

ftpClient.enterLocalPassiveMode();



Probleme mit einem FTP-Server im passiven Modus

Falls Sie einen FTP-Server unter Windows im passiven Modus hinter einer Firewall betreiben, müssen Sie eventuell die statusbehaftete FTP-Filterung deaktivieren:

netsh advfirewall set global statefulftp disable

Genaueres siehe: Windows-Firewall-Konfiguration für FTP-Server im passiven Modus.



Probleme beim Unzip

Falls Sie beim Unzippen (z.B. aus einem FTP-Stream) folgende Fehlermeldung erhalten:

java.util.zip.ZipException: invalid distance too far back

Dann ändern Sie folgendermaßen den Transfer-Mode und den File-Type:

ftpClient.setFileTransferMode( FTP.BINARY_FILE_TYPE );

ftpClient.setFileType( FTP.BINARY_FILE_TYPE );

Der Grund ist, dass andernfalls Zeilenendezeichen (CR, LF, CRLF) interpretiert und gegebenenfalls geändert werden, weil sie unter den verschiedenen Betriebssystemen verschieden gesetzt werden. Beispielsweise würden bei einer unter Linux erstellten Textdatei die Zeilen mit LF (= \n) beendet, während unter Windows die zwei Zeichen CR und LF (= \r\n) verwendet werden.

Falls Sie beim Unzippen (z.B. aus einem FTP-Stream) folgende Fehlermeldung erhalten:

java.lang.IllegalArgumentException: MALFORMED
at java.util.zip.ZipCoder.toString

Dann haben Sie wahrscheinlich ein Character-Encoding-Problem beim Aufruf von ZipInputStream.getNextEntry() wegen Umlauten und Sonderzeichen im Zip-Entry-Namen. Falls Sie eine Code-Zeile haben ähnlich zu:

ZipInputStream zin = new ZipInputStream( meinInputStream );

Dann erweitern Sie sie um den passenden Character-Encoding-Charset:

ZipInputStream zin = new ZipInputStream( meinInputStream, StandardCharsets.ISO_8859_1 );

bzw.:

ZipInputStream zin = new ZipInputStream( meinInputStream, StandardCharsets.UTF_8 );

Analoges gilt natürlich ebenso für ZipOutputStream.



Probleme beim Append

Bei einigen FTP-Servern, beispielsweise beim ProFTPD, sind einige FTP-Kommandos defaultmäßig disabled, zum Beispiel das APPE-Kommando zum Anhängen an Dateien.

Beispiele für mögliche Fehlermeldungen:

451 Append/Restart not permitted

oder

550 APPE not compatible with server configuration

In solchen Fällen kann in der FTP-Server-Konfiguration das entsprechende Feature freigeschaltet werden.

Beispielsweise muss beim ProFTPD zur Freigabe des APPE-Kommandos in der proftpd.conf (z.B. in der <Global>-Sektion) eingetragen werden (siehe auch ProFTPD-FAQ):

AllowOverwrite on

AllowRetrieveRestart on

AllowStoreRestart on

HiddenStores off



Probleme wegen "Connection closed"

Falls Sie ähnlich lautende Fehlermeldungen erhalten wie:

551 Error on output file

org.apache.commons.net.ftp.FTPConnectionClosedException: Connection closed without indication

org.apache.commons.net.ftp.FTPConnectionClosedException: FTP response 421 received. Server closed connection

Dann sollten Sie prüfen, ob Sie zwischen einzelnen Schreibkommandos z.B. in einen OutputStream zu lange Pausen haben. Eventuell helfen dann häufigere flush()-Aufrufe.

Denken Sie daran, dass die meisten FTP-Server zwei verschiedene Timeouts haben, den Session-Timeout (z.B. idle_session_timeout) und den oft kürzeren Daten-Timeout (z.B. data_connection_timeout).



Wrapper für FTP-Server / Fileserver

Das folgende Beispiel zeigt einen Wrapper, über den transparent konfigurierbar wahlweise auf einen Fileserver oder FTP-Server zugegriffen werden kann. Der JUnit-Test zeigt beide Optionen. Das Beispiel basiert wieder wie das vorherige Beispiel auf FTPClient und FtpServer.

Folgendermaßen führen Sie das Beispiel aus (alternativ können Sie es auch downloaden):

  1. Das Java SE JDK und Maven müssen installiert sein.

  2. Öffnen Sie ein Kommandozeilenfenster, wechseln Sie in Ihr Projekte-Verzeichnis (z.B. D:\MeinWorkspace) und erzeugen Sie eine neue Projektstruktur:

    cd \MeinWorkspace

    md FtpWrapper

    cd FtpWrapper

    md src\main\java\de\meinefirma\meinprojekt\ftp

    md src\test\java\de\meinefirma\meinprojekt\ftp

    md src\test\resources

  3. Erstellen Sie im FtpWrapper-Projektverzeichnis die Projektkonfiguration: pom.xml

    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
      <modelVersion>4.0.0</modelVersion>
      <groupId>de.meinefirma.meinprojekt</groupId>
      <artifactId>FtpWrapper</artifactId>
      <version>1.0-SNAPSHOT</version>
      <packaging>jar</packaging>
      <name>FtpWrapper</name>
      <build>
        <plugins>
          <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.3</version>
            <configuration>
              <source>1.7</source>
              <target>1.7</target>
            </configuration>
          </plugin>
        </plugins>
      </build>
      <dependencies>
        <dependency>
          <groupId>commons-net</groupId>
          <artifactId>commons-net</artifactId>
          <version>3.3</version>
        </dependency>
        <dependency>
          <groupId>org.apache.ftpserver</groupId>
          <artifactId>ftpserver-core</artifactId>
          <version>1.0.6</version>
          <scope>test</scope>
        </dependency>
        <dependency>
          <groupId>log4j</groupId>
          <artifactId>log4j</artifactId>
          <version>1.2.17</version>
        </dependency>
        <dependency>
          <groupId>org.slf4j</groupId>
          <artifactId>slf4j-api</artifactId>
          <version>1.7.12</version>
          <scope>test</scope>
        </dependency>
        <dependency>
          <groupId>org.slf4j</groupId>
          <artifactId>slf4j-log4j12</artifactId>
          <version>1.7.12</version>
          <scope>test</scope>
        </dependency>
        <dependency>
          <groupId>junit</groupId>
          <artifactId>junit</artifactId>
          <version>4.12</version>
          <scope>test</scope>
        </dependency>
      </dependencies>
    </project>
    
  4. Erstellen Sie im src\test\resources-Verzeichnis folgende Properties-Dateien:

    FtpFileWrapper-FS.properties

    FsRoot=target/FileserverHome
    

    FtpFileWrapper-FTP.properties

    FtpHost=localhost
    FtpPort=2121
    FtpUsr=WriteUserName
    FtpPwd=WriteUserPwd
    
  5. Erstellen Sie im src\main\java\de\meinefirma\meinprojekt\ftp-Verzeichnis die Klasse: FtpFileWrapper.java

    package de.meinefirma.meinprojekt.ftp;
    
    import java.io.*;
    import java.net.*;
    import java.util.*;
    import java.util.concurrent.*;
    import java.util.concurrent.atomic.AtomicLong;
    import java.util.regex.Pattern;
    import java.util.zip.*;
    import org.apache.commons.net.*;
    import org.apache.commons.net.ftp.*;
    import org.apache.log4j.Logger;
    
    /**
     * <b> Wrapper fuer transparente Dateizugriffe entweder per FTP oder per Dateisystem (lokal oder Fileserver). </b><br>
     * <br>
     * Beispielsweise kann konfiguriert werden, dass Dateizugriffe in der Produktionsumgebung per FTP,
     * aber in lokalen Entwicklertestumgebungen per lokalem Dateisystem erfolgen.<br>
     * Die FTP-Methoden basieren auf Apache FTPClient:
     * {@link "http://commons.apache.org/net/apidocs/org/apache/commons/net/ftp/FTPClient.html"}
     * <br>
     * TASKS_COUNT sollte auf ca. ein Zehntel der Anzahl der zur Verfuegung stehenden dynamischen Ports (bzw. File-Handles) gesetzt werden.
     */
    public final class FtpFileWrapper
    {
       private static final Logger  LOGGER = Logger.getLogger( FtpFileWrapper.class );
       public  static final boolean LOG_ALL_CONSOLE = false;
       public  static final boolean PASV            = true;
       public  static final String  FS_ROOT_KEY     = "FsRoot";
       public  static final String  FTP_URI_KEY     = "FtpUri";
       public  static final String  FTP_USR_KEY     = "FtpUsr";
       public  static final String  FTP_PWD_KEY     = "FtpPwd";
       public  static final String  FTP_HOST_KEY    = "FtpHost";
       public  static final String  FTP_PORT_KEY    = "FtpPort";
       public  static final String  FTP_PATH_KEY    = "FtpPath";
       public  static final String  FTP_PROT_URI    = "ftp://";
       public  static final String  TEMP_MARK       = "$$";
       public  static final int     UNZIP_MIN_MS    = 120;
       private static final int     BUFF_SIZE       = 32768;
       private static final long    RETRY_PAUSE_MS  = TimeUnit.MINUTES.toMillis( 1 );
       private static final int     RETRY_COUNT     = 5;
       private static final int     TASKS_COUNT     = 450;
       private static final int     TASKS_WAIT_MINUTEN = 2;
       private static final int     TASKS_WAIT_REDUCE  = TASKS_WAIT_MINUTEN * 50;
       private static final long    NANOSEK_PRO_MILLISEK = 1000000;
       private static final AtomicLong  LOGIN_COUNT      = new AtomicLong();
       private static final Queue<Long> TASKS_TIME       = new ConcurrentLinkedQueue<Long>();
       private String     fsRootDir;
       private String     fsActSubDir;
       private String     ftpRootPath;
       private String     ftpActSubDir;
       private Properties ftpOderFileProps;
       private boolean    ftpOrFile;
       private FTPClient  ftpClient = null;
       private boolean    propsOk   = false;
       private boolean    loggedin  = false;
    
       /**
        * Privater Konstruktor.
        * @param  ftpOderFileProps Konfigurationsparameter entweder fuer FTP oder fuer Dateisystem-Root
        */
       private FtpFileWrapper( Properties ftpOderFileProps )
       {
          this.ftpOderFileProps = ftpOderFileProps;
       }
    
       /**
        * Erzeuge weiteren FtpFileWrapper.
        */
       public FtpFileWrapper createSecondFtpFileWrapper()
       {
          return new FtpFileWrapper( ftpOderFileProps );
       }
    
       /**
        * Erzeuge FtpFileWrapper mit einem Properties-Objekt.
        * @param  ftpOderFileProps Konfigurationsparameter entweder fuer FTP oder fuer Dateisystem-Root
        */
       public static FtpFileWrapper createFtpFileWrapperFromProps( Properties ftpOderFileProps )
       {
          return new FtpFileWrapper( ftpOderFileProps );
       }
    
       /**
        * Erzeuge FtpFileWrapper mit einer Properties-Datei (entweder im CLASSPATH oder per Pfad).
        * @param  propFile Properties-Datei mit Konfigurationsparametern entweder fuer FTP oder fuer Dateisystem-Root
        */
       public static FtpFileWrapper createFtpFileWrapperFromPropFile( String propFile )
       {
          return new FtpFileWrapper( loadPropertiesFromFileFromDirOrClasspath( propFile ) );
       }
    
       /**
        * Erzeuge FtpFileWrapper aus URI (Fileserver-URI oder FTP-URI).
        * @param  uri Fileserver-URI oder FTP-URI (FTP-URI muss mit "ftp://" beginnen)
        */
       public static FtpFileWrapper createFtpFileWrapperFromUri( String uri )
       {
          Properties p = new Properties();
          p.put( FTP_URI_KEY, uri );
          return new FtpFileWrapper( p );
       }
    
       /**
        * Erzeuge FTP-FtpFileWrapper zu FTP-Parametern.
        */
       public static FtpFileWrapper createFtpFileWrapperFromFtpParms( String host, String port, String path, String usr, String pwd )
       {
          Properties p = new Properties();
          p.put( FTP_HOST_KEY, host );
          p.put( FTP_PORT_KEY, port );
          p.put( FTP_PATH_KEY, path );
          p.put( FTP_USR_KEY,  usr );
          p.put( FTP_PWD_KEY,  pwd );
          return new FtpFileWrapper( p );
       }
    
       /**
        * Um nicht in zu kurzer Zeit zu viele Logins durchzufuehren und Ports zu belegen,
        * werden bei einigen Methoden Zeitstempel gespeichert und eventuell wird verzoegert.
        */
       private static void registerTasktimeAndDelay()
       {
          long actTime = (new Date()).getTime();
          if( TASKS_TIME.isEmpty() ) {
             TASKS_TIME.add( Long.valueOf( actTime ) );
             return;
          }
          Long oldestTime = TASKS_TIME.peek();
          int  n;
          while( (n = TASKS_TIME.size()) > TASKS_COUNT ) {
             oldestTime = TASKS_TIME.poll();
          }
          long diffOldestTime = actTime - oldestTime.longValue();
          long waitTime = TimeUnit.MINUTES.toMillis( TASKS_WAIT_MINUTEN ) - diffOldestTime;
          waitTime = Math.max( 0, waitTime ) * n / TASKS_COUNT / TASKS_WAIT_REDUCE;
          if( waitTime > 0 ) {
             if( waitTime > TimeUnit.MINUTES.toMillis( TASKS_WAIT_MINUTEN ) / TASKS_WAIT_REDUCE / 2 ) {
                String diffOldestTimeString = ( diffOldestTime > 1000 ) ? ((diffOldestTime / 1000) + " s") : (diffOldestTime + " ms");
                LOGGER.info( "FTP: " + ((waitTime / 100L) / 10.) + " s warten, da es in den letzten " + diffOldestTimeString + " mehr als " + n + " FTP-Tasks gab." );
             }
             try { Thread.sleep( waitTime ); } catch( InterruptedException e ) {/*ok*/}
          }
          TASKS_TIME.add( Long.valueOf( (new Date()).getTime() ) );
       }
    
       /**
        * Vor Dateizugriffen muss Login und hinterher Logout erfolgen.
        * @return true falls erfolgreich
        */
       public boolean login() throws IOException
       {
          registerTasktimeAndDelay();
          LOGIN_COUNT.incrementAndGet();
          if( loggedin ) { logout(); }
          ftpActSubDir = null;
          // Properties ueberpruefen:
          if( ftpOderFileProps == null || ftpOderFileProps.isEmpty() ) {
             throw new IllegalArgumentException( "Fehler: Properties fehlen." );
          }
          if( checkStringNichtLeer( ftpOderFileProps.getProperty( FTP_HOST_KEY ) ) &&
              checkStringNichtLeer( ftpOderFileProps.getProperty( FTP_PORT_KEY ) ) ) {
             propsOk   = true;
             ftpOrFile = true;
          } else if( checkStringNichtLeer( ftpOderFileProps.getProperty( FTP_URI_KEY ) ) ) {
             String uri = ftpOderFileProps.getProperty( FTP_URI_KEY );
             ftpOrFile = uri != null && uri.trim().toLowerCase( Locale.GERMAN ).startsWith( FTP_PROT_URI );
             propsOk   = true;
             if( ftpOrFile ) {
                Properties p = extractFtpParmsFromUri( uri );
                if( checkStringNichtLeer( p.getProperty( FTP_HOST_KEY ) ) &&
                    checkStringNichtLeer( p.getProperty( FTP_PORT_KEY ) ) ) {
                   ftpOderFileProps = p;
                } else {
                   propsOk = false;
                }
             } else {
                ftpOderFileProps.setProperty( FS_ROOT_KEY, uri );
             }
          } else if( checkStringNichtLeer( ftpOderFileProps.getProperty( FS_ROOT_KEY ) ) ) {
             propsOk   = true;
             ftpOrFile = false;
          }
          if( !propsOk ) {
             throw new IllegalArgumentException( "Fehler: Properties entweder zum FTP-Zugang oder zur Fileserver-Root fehlen." );
          }
          if( ftpOrFile ) {
             // FTP:
             int port = 0;
             try {
                port = Integer.parseInt( ftpOderFileProps.getProperty( FTP_PORT_KEY ).trim() );
             } catch( NumberFormatException e ) {/* nothing to do*/}
             if( port == 0 ) {
                throw new IllegalArgumentException( "Fehler: Properties zum FTP-Zugang muessen gueltige Portnummer enthalten." );
             }
             ftpClient = new FTPClient();
             if( LOG_ALL_CONSOLE ) { ftpClient.addProtocolCommandListener( new ConsoleProtocolCommandListener() ); }
             ftpClient.connect( ftpOderFileProps.getProperty( FTP_HOST_KEY ).trim(), port );
             try {
                ftpClient.login( ftpOderFileProps.getProperty( FTP_USR_KEY ),
                                 ftpOderFileProps.getProperty( FTP_PWD_KEY ) );
                loggedin = FTPReply.isPositiveCompletion( ftpClient.getReplyCode() );
                if( !loggedin ) {
                   throw new IOException( "FTP-Fehler: " + Arrays.asList( ftpClient.getReplyStrings() ) );
                }
                ftpRootPath = ftpOderFileProps.getProperty( FTP_PATH_KEY );
                if( ftpRootPath != null ) { ftpRootPath = ftpRootPath.trim(); }
                if( ftpRootPath != null && ftpRootPath.length() > 0 ) {
                   if( !ftpClient.changeWorkingDirectory( ftpRootPath ) ) {
                      throw new IOException( "FTP-Fehler mit FTP-Root-Path '" + ftpRootPath + "': " + Arrays.asList( ftpClient.getReplyStrings() ) );
                   }
                }
                if( PASV ) { ftpClient.enterLocalPassiveMode(); }
                ftpClient.setFileTransferMode( FTP.BINARY_FILE_TYPE );
                ftpClient.setFileType( FTP.BINARY_FILE_TYPE );
             } catch( IOException ex ) {
                ftpClient.disconnect();
                ftpClient = null;
                throw ex;
             }
          } else {
             // Dateisystem:
             fsActSubDir = "";
             fsRootDir = ftpOderFileProps.getProperty( FS_ROOT_KEY );
             if( !checkStringNichtLeer( fsRootDir ) ) {
                throw new IllegalArgumentException( "Fehler: Fileserver-Root-Verzeichnis muss angegeben werden." );
             }
             fsRootDir = fsRootDir.trim();
             File fsRootDirFl = new File( fsRootDir );
             loggedin = fsRootDirFl.exists() && fsRootDirFl.isDirectory();
             if( !loggedin ) {
                loggedin = fsRootDirFl.mkdirs();
             }
          }
          return loggedin;
       }
    
       /**
        * Vor Dateizugriffen muss Login und hinterher Logout erfolgen.
        */
       public void logout()
       {
          loggedin = false;
          ftpActSubDir = null;
          if( ftpClient != null ) {
             try {
                try {
                   ftpClient.logout();
                } finally {
                   ftpClient.disconnect();
                }
             } catch( IOException ex ) {
                /* nothing to do*/
             } finally {
                ftpClient = null;
             }
          }
       }
    
       /**
        * FTP- oder Fileserver-Betrieb.
        * @return true falls FTP, false falls Fileserver
        */
       public boolean getFtpOrFile()
       {
          loginIfNotLoggedInISE();
          return ftpOrFile;
       }
    
       /**
        * Einige wenige Methoden erfordern dieses zusaetzliche abschliessende Kommando,
        * z.B. die Methoden, welche {@link FTPClient#retrieveFileStream()} verwenden
        * (insbesondere {@link FtpFileWrapper#retrieveTextFile()}, {@link FtpFileWrapper#retrieveBinaryFile()}
        *  und {@link FtpFileWrapper#storeFile( String remoteResultFile )}).<br>
        * Siehe {@link "http://commons.apache.org/net/apidocs/org/apache/commons/net/ftp/FTPClient.html#completePendingCommand()"}
        */
       public boolean completePendingCommand()
       {
          if( ftpOrFile ) {
             try {
                // if( FTPReply.isPositiveIntermediate( ftpClient.getReplyCode() ) )
                ftpClient.completePendingCommand();
             } catch( IOException ex ) {
                LOGGER.info( "completePendingCommand(): ", ex );
             }
             return FTPReply.isPositiveCompletion( ftpClient.getReplyCode() );
          }
          return true;
       }
    
       /**
        * Wechseln des aktuellen Verzeichnisses.
        * @param  subDir neues Verzeichnis
        * @return true falls erfolgreich
        */
       public boolean changeWorkingDirectory( String subDir ) throws IOException
       {
          loginIfNotLoggedIn();
          if( subDir == null || subDir.trim().length() == 0 ) { return true; }
          String verz = subDir.trim();
          if( ftpOrFile ) {
             verz = verz.replace( '\\', '/' );
             if( verz.startsWith( "/" ) && ftpRootPath != null ) {
                verz = ftpRootPath + verz;
                if( !verz.startsWith( "/" ) ) { verz = "/" + verz; }
             }
             if( ftpClient.changeWorkingDirectory( verz ) ) {
                ftpActSubDir = ftpClient.printWorkingDirectory();
                return true;
             }
             return false;
          }
          String fsActSubDirOld = fsActSubDir;
          if( verz.startsWith( "/" ) || verz.startsWith( "\\" ) ) {
             fsActSubDir = ( verz.length() > 1 ) ? verz : "";
          } else {
             fsActSubDir = fsActSubDir + File.separator + verz;
          }
          if( (new File( fsRootDir + fsActSubDir )).exists() ) {
             return true;
          }
          fsActSubDir = fsActSubDirOld;
          return false;
       }
    
       /**
        * Returniere das aktuelle Verzeichnis.
        * @return aktuelles Verzeichnis
        */
       public String getWorkingDirectory() throws IOException
       {
          loginIfNotLoggedIn();
          if( ftpOrFile ) {
             String dir = ftpClient.printWorkingDirectory();
             if( ftpRootPath != null && ftpRootPath.length() > 0 ) {
                int i = dir.indexOf( ftpRootPath );
                dir = dir.substring( i + ftpRootPath.length() );
             }
             return ( dir != null && dir.length() > 0 ) ? dir : "/";
          }
          return fsActSubDir;
       }
    
       /**
        * Erstellen eines neuen Verzeichnisses.
        * @param  subDir neues Verzeichnis
        * @return true falls erfolgreich
        */
       public boolean makeDirectory( String subDir ) throws IOException
       {
          loginIfNotLoggedIn();
          if( subDir == null || subDir.trim().length() == 0 ) { return false; }
          String verz = subDir.trim();
          if( ftpOrFile ) {
             verz = verz.replace( '\\', '/' );
             if( verz.endsWith( "/" ) ) { verz = verz.substring( 0, verz.length() - 1 ); }
             String[]      vv = verz.split( "/" );
             StringBuilder sb = new StringBuilder();
             boolean       b  = false;
             if( vv[0].length() == 0 && ftpRootPath != null ) {
                vv[0] = ftpRootPath;
             }
             for( int i = 0; i < vv.length; i++ ) {
                sb.append( vv[i].trim() );
                if( sb.length() > 0 ) { b = ftpClient.makeDirectory( sb.toString() ); }
                sb.append( "/" );
             }
             return b;
          }
          return (new File( getFsRootActPath( verz ) + verz )).mkdirs();
       }
    
       /**
        * Lesen von Verzeichnisnamen.
        * @return Liste der Verzeichnisnamen
        */
       public List<String> listDirNames() throws IOException
       {
          return listNames( null, true, true );
       }
       /**
        * Lesen von Verzeichnisnamen in einem Unterverzeichnis.
        * @param  subDir Lesen der Verzeichnisnamen in diesem Unterverzeichnis
        * @return Liste der Verzeichnisnamen
        */
       public List<String> listDirNames( String subDir ) throws IOException
       {
          return listNames( subDir, true, true );
       }
       /**
        * Lesen von Dateinamen.
        * @return Liste der Dateinamen
        */
       public List<String> listFileNames() throws IOException
       {
          return listNames( null, false, true );
       }
       /**
        * Lesen von Dateinamen in einem Unterverzeichnis.
        * @param  subDir Lesen der Dateinamen in diesem Unterverzeichnis
        * @return Liste der Dateinamen
        */
       public List<String> listFileNames( String subDir ) throws IOException
       {
          return listNames( subDir, false, true );
       }
    
       /**
        * Lesen entweder von Verzeichnisnamen oder von Dateinamen in einem Unterverzeichnis.
        * @param  dirOrFiles    true falls Verzeichnisnamen gelesen werden sollen oder false fuer Dateinamen
        * @param  subDir        Unterverzeichnis
        * @param  ohneTempFiles true falls auf "$$" (= TEMP_MARK) endende Dateien nicht beruecksichtigt werden sollen
        * @return Liste der Verzeichnisnamen bzw. Dateinamen
        */
       private List<String> listNames( String subDir, final boolean dirOrFiles, final boolean ohneTempFiles ) throws IOException
       {
          loginIfNotLoggedIn();
          List<String> namesList = new ArrayList<String>();
          if( ftpOrFile ) {
             // FTP:
             FTPFile[] files = listFtpFiles( subDir, false );
             if( files != null ) {
                for( FTPFile f : files ) {
                   if( f != null && ((dirOrFiles && f.isDirectory()) || (!dirOrFiles && f.isFile()))
                       && (!ohneTempFiles || !f.getName().endsWith( TEMP_MARK )) ) {
                          namesList.add( f.getName() );
                   }
                }
             }
             return namesList;
          }
          // Dateisystem:
          File[] files = listFsFiles( subDir );
          if( files != null ) {
             for( File f : files ) {
                if( f != null && ((dirOrFiles && f.isDirectory()) || (!dirOrFiles && f.isFile()))
                    && (!ohneTempFiles || !f.getName().endsWith( TEMP_MARK )) ) {
                       namesList.add( f.getName() );
                }
             }
          }
          return namesList;
       }
    
       /**
        * Ermittle die Groesse einer Remote-Datei.
        * @param  file Remote-Datei
        * @return Groesse der Remote-Datei (in Bytes)
        */
       public long getLength( String file )
       {
          if( file == null || file.trim().length() == 0 ) {
             return 0;
          }
          loginIfNotLoggedInISE();
          if( ftpOrFile ) {
             // FTP:
             File   fl = new File( file.trim() );
             String parentPath = fl.getParent();
             String fileName   = fl.getName();
             if( fileName == null || fileName.length() == 0 ) { return 0; }
             try {
                FTPFile[] files = listFtpFiles( parentPath, false );
                if( files != null ) {
                   for( FTPFile f : files ) {
                      if( f != null && fileName.equals( f.getName() ) ) {
                         return f.getSize();
                      }
                   }
                }
             } catch( IOException ex ) {/* nothing to do */}
             return 0;
          }
          // Dateisystem:
          return (new File( getFsRootActPath( file ) + file )).length();
       }
    
       /**
        * Ermittle Anzahl der Remote-Dateien und Summe der Dateigroessen der Remote-Dateien.
        * @param  subDir     Unterverzeichnis
        * @param  ignoreDirs zu ignorierende Unterverzeichnisse
        * @return Zwei Werte: a) Anzahl Dateien, b) Summe der Dateigroessen in Byte
        */
       public long[] getFileCountAndSizeSum( String subDir, Set<String> ignoreDirs )
       {
          loginIfNotLoggedInISE();
          long[] countAndSizeSum = new long[2];
          List<FtpFsFileData> fds = listFtpFsFileData( subDir, false );
          if( fds == null ) { return null; }
          for( FtpFsFileData f : fds ) {
             if( f.isFile ) {
                countAndSizeSum[0] = countAndSizeSum[0] + 1;
                countAndSizeSum[1] = countAndSizeSum[1] + f.size;
             } else if( f.isDirectory ) {
                if( ignoreDirs == null || !ignoreDirs.contains( f.name ) ) {
                   String dir = ( subDir == null || subDir.trim().length() == 0 ) ? "" : subDir + "/";
                   long[] countAndSizeSumDir = getFileCountAndSizeSum( dir + f.name, ignoreDirs );
                   countAndSizeSum[0] += countAndSizeSumDir[0];
                   countAndSizeSum[1] += countAndSizeSumDir[1];
                }
             }
          }
          return countAndSizeSum;
       }
    
       /**
        * Ermittle den Zeitstempel der aeltesten Remote-Datei.
        * @param  startDir                 Unterverzeichnis
        * @param  regexAllowedCompleteDirs Regex-Ausdruck fuer zu verwendende komplette Unterverzeichnispfade (also inkl. aller Unterverzeichnisse)
        * @param  ignoreSingleDirs         zu ignorierende Unterverzeichnisse, aber nicht komplette Unterverzeichnispfade, sondern einzelne Unterverzeichnisse
        * @return FileData zur aeltesten Remote-Datei
        */
       public FtpFsFileData getOldestFileData( String startDir, String regexAllowedCompleteDirs, Set<String> ignoreSingleDirs )
       {
          loginIfNotLoggedInISE();
          List<FtpFsFileData> fds = listFtpFsFileData( startDir, false );
          if( fds == null ) { return null; }
          String prefixDir = ( startDir == null || startDir.trim().length() == 0 ) ? "" : (( startDir.trim().endsWith( "/" ) ) ? startDir.trim() : startDir.trim() + "/");
          Pattern regexAllowedCompleteDirsPattern = ( regexAllowedCompleteDirs != null && regexAllowedCompleteDirs.trim().length() > 0 ) ? Pattern.compile( regexAllowedCompleteDirs.trim() ) : null;
          boolean noRootFiles = ignoreSingleDirs != null && ignoreSingleDirs.contains( "/" );
          FtpFsFileData fdOldest = null;
          for( FtpFsFileData fd : fds ) {
             if( fd.isFile ) {
                if( (!noRootFiles || !"/".equals( prefixDir )) &&
                      (fdOldest == null || fdOldest.timestamp.after( fd.timestamp )) ) {
                   fdOldest = fd;
                }
             } else if( fd.isDirectory ) {
                if( (regexAllowedCompleteDirsPattern == null || regexAllowedCompleteDirsPattern.matcher( prefixDir + fd.name ).matches()) &&
                      (ignoreSingleDirs == null || !ignoreSingleDirs.contains( fd.name )) ) {
                   FtpFsFileData fdAct = getOldestFileData( prefixDir + fd.name, regexAllowedCompleteDirs, ignoreSingleDirs );
                   if( fdAct != null && (fdOldest == null || fdOldest.timestamp.after( fdAct.timestamp )) ) {
                      fdOldest = fdAct;
                   }
                }
             }
          }
          return fdOldest;
       }
    
       /**
        * Returniert ob eine Remote-Datei existiert.
        * @param  file Remote-Datei
        * @return true falls Remote-Datei existiert
        */
       public boolean existsFile( String file )
       {
          loginIfNotLoggedInISE();
          if( file == null || file.trim().length() == 0 ) { return false; }
          if( ftpOrFile ) {
             // FTP:
             File   f = new File( file.trim() );
             String parentPath = f.getParent();
             String fileName   = f.getName();
             try {
                List<String> filenames = listNames( parentPath, false, false );
                return filenames != null && filenames.contains( fileName );
             } catch (IOException e) {
                return false;
             }
          }
          // Dateisystem:
          File f = new File( getFsRootActPath( file ) + file );
          return f.exists() && f.isFile();
       }
    
       /**
        * Returniert ob eine Remote-Datei existiert (versucht es mehrmals).
        * @param  file Remote-Datei
        * @return true falls Remote-Datei existiert
        */
       public boolean existsFileWithLoop( String file )
       {
          boolean b = false;
          int i = 0;
          while( !b && i++ <= RETRY_COUNT ) {
             b = existsFile( file );
             if( !b ) { try { Thread.sleep( RETRY_PAUSE_MS ); } catch( InterruptedException e ) {/*ok*/} }
          }
          if( b && i > 1 ) {
             LOGGER.warn( "FTP-Warnung: Datei '" + file + "' erst beim " + i + ". Versuch gefunden." );
          }
          return b;
       }
    
       /**
        * Returniert ob ein Remote-Verzeichnis existiert.
        * @param  dir  Remote-Verzeichnis
        * @return true falls Remote-Verzeichnis existiert
        */
       public boolean existsDir( String dir )
       {
          loginIfNotLoggedInISE();
          if( dir == null || dir.trim().length() == 0 ) { return false; }
          if( ftpOrFile ) {
             // FTP:
             File   f = new File( dir.trim() );
             String parentPath = f.getParent();
             String dirName    = f.getName();
             try {
                List<String> dirnames = listNames( parentPath, true, false );
                return dirnames != null && dirnames.contains( dirName );
             } catch (IOException e) {
                return false;
             }
          }
          // Dateisystem:
          File f = new File( getFsRootActPath( dir ) + dir );
          return f.exists() && f.isDirectory();
       }
    
       /**
        * Loesche Datei.
        * @param  file zu entfernende Remote-Datei
        * @return true falls erfolgreich
        */
       public boolean deleteFile( String file ) throws IOException
       {
          loginIfNotLoggedIn();
          if( file == null || file.trim().length() == 0 ) { return false; }
          if( ftpOrFile ) {
             String fl = file.trim().replace( '\\', '/' );
             if( fl.startsWith( "/" ) && ftpRootPath != null && ftpRootPath.length() > 0 ) {
                fl = ftpRootPath + fl;
             }
             return ftpClient.deleteFile( fl );
          }
          return (new File( getFsRootActPath( file ) + file )).delete();
       }
    
       /**
        * Loesche Verzeichnis inklusive aller Unterverzeichnisse und aller enthaltenen Dateien.
        * @param  dir  zu loeschendes Remote-Verzeichnis
        * @return true falls erfolgreich
        */
       public boolean removeDirectory( String dir ) throws IOException
       {
          loginIfNotLoggedIn();
          if( dir == null || dir.trim().length() == 0 ) { return false; }
          boolean b = true;
          List<FtpFsFileData> fds = listFtpFsFileData( dir, false );
          if( fds == null ) { return false; }
          for( FtpFsFileData f : fds ) {
             String fn = dir + File.separator + f.name;
             if( f.isFile ) {
                b &= ( ftpOrFile )
                     ? ftpClient.deleteFile( fn.replace( '\\', '/' ) )
                     : (new File( getFsRootActPath( fn ) + fn )).delete();
             } else if( f.isDirectory ) {
                b &= removeDirectory( fn );
             }
          }
          b &= ( ftpOrFile )
               ? ftpClient.removeDirectory( dir.replace( '\\', '/' ) )
               : (new File( getFsRootActPath( dir ) + dir )).delete();
          return b;
       }
    
       /**
        * Umbenenne Datei.
        * @param oldFilename Vorheriger Dateiname
        * @param newFilename Neuer Dateiname
        * @return true falls erfolgreich
        */
       public boolean renameFile( String oldFilename, String newFilename ) throws IOException
       {
          loginIfNotLoggedIn();
          if( oldFilename == null || oldFilename.trim().length() == 0 ||
              newFilename == null || newFilename.trim().length() == 0 ||
              oldFilename.trim().equals( newFilename.trim() ) ) {
             return false;
          }
          if( ftpOrFile ) {
             String oldFn = oldFilename.trim().replace( '\\', '/' );
             String newFn = newFilename.trim().replace( '\\', '/' );
             if( oldFn.startsWith( "/" ) && ftpRootPath != null ) { oldFn = ftpRootPath + oldFn; }
             if( newFn.startsWith( "/" ) && ftpRootPath != null ) { newFn = ftpRootPath + newFn; }
             return ftpClient.rename( oldFn, newFn );
          }
          return (new File( getFsRootActPath( oldFilename ) + oldFilename.trim() )).renameTo(
                  new File( getFsRootActPath( newFilename ) + newFilename.trim() ) );
       }
    
       /**
        * Kopiere Remote-Datei in andere Remote-Datei (bei beiden Dateien kann auch ein Unterverzeichnis angegeben werden).
        * @param  remoteSourceFile Remote-Quell-Datei
        * @param  remoteResultFile Remote-Ergebnis-Datei
        * @return true falls erfolgreich
        */
       public boolean copyRemoteFile( String remoteSourceFile, String remoteResultFile ) throws IOException
       {
          if( remoteSourceFile == null || remoteResultFile == null ) { throw new IllegalArgumentException(); }
          if( remoteSourceFile.equals( remoteResultFile ) ) { return false; }
          FtpFileWrapper ffwrapSource = new FtpFileWrapper( ftpOderFileProps );
          FtpFileWrapper ffwrapResult = new FtpFileWrapper( ftpOderFileProps );
          ffwrapSource.login();
          try {
             ffwrapResult.login();
             try {
                String wd = getWorkingDirectory();
                if( wd != null && wd.trim().length() > 0 && !"/".equals( wd ) ) {
                   ffwrapSource.changeWorkingDirectory( wd );
                   ffwrapResult.changeWorkingDirectory( wd );
                }
                return copyRemoteFile( ffwrapSource, remoteSourceFile, ffwrapResult, remoteResultFile );
             } finally {
                ffwrapResult.logout();
             }
          } finally {
             ffwrapSource.logout();
          }
       }
    
       /**
        * Kopiere Remote-Datei vom Source-FtpFileWrapper in andere Remote-Datei vom Result-FtpFileWrapper
        * (bei beiden Dateien kann auch ein Unterverzeichnis angegeben werden).<br>
        * Zu den beiden FtpFileWrapper erfolgt kein logout(), dieser muss extern erfolgen.<br>
        * Anders <tt>copyRemoteFileWithNoRetry()</tt> fuehrt diese Methode bei Fehlschlag mehrere Versuche aus.
        * @param  ffwrapSource     FtpFileWrapper fuer Remote-Quell-Datei
        * @param  remoteSourceFile Remote-Quell-Datei
        * @param  ffwrapResult     FtpFileWrapper fuer Remote-Ergebnis-Datei
        * @param  remoteResultFile Remote-Ergebnis-Datei
        * @return true falls erfolgreich
        */
       public static boolean copyRemoteFile( FtpFileWrapper ffwrapSource, String remoteSourceFile, FtpFileWrapper ffwrapResult, String remoteResultFile ) throws IOException
       {
          int i = 0;
          while( true ) {
             try {
                copyRemoteFileWithNoRetry( ffwrapSource, remoteSourceFile, ffwrapResult, remoteResultFile );
                if( i > 0 ) {
                   LOGGER.warn( "FTP-Warnung: Copy von '" + remoteSourceFile + "' nach '" + remoteResultFile + "' erst nach " + i + " Minuten erfolgreich." );
                }
                return true;
             } catch( Exception ex ) {
                if( i++ >= RETRY_COUNT ) {
                   throw new IOException( "Fehler: Copy von '" + remoteSourceFile + "' nach '" + remoteResultFile + "' fehlgeschlagen. ", ex );
                }
                try { Thread.sleep( RETRY_PAUSE_MS ); } catch( InterruptedException e ) {/* ok */}
             }
          }
       }
    
       /**
        * Kopiere Remote-Datei vom Source-FtpFileWrapper in andere Remote-Datei vom Result-FtpFileWrapper
        * (bei beiden Dateien kann auch ein Unterverzeichnis angegeben werden).<br>
        * Zu den beiden FtpFileWrapper erfolgt kein logout(), dieser muss extern erfolgen.<br>
        * Anders <tt>copyRemoteFile()</tt> fuehrt diese Methode bei Fehlschlag nicht mehrere Versuche aus.
        * @param  ffwrapSource     FtpFileWrapper fuer Remote-Quell-Datei
        * @param  remoteSourceFile Remote-Quell-Datei
        * @param  ffwrapResult     FtpFileWrapper fuer Remote-Ergebnis-Datei
        * @param  remoteResultFile Remote-Ergebnis-Datei
        */
       public static void copyRemoteFileWithNoRetry( FtpFileWrapper ffwrapSource, String remoteSourceFile, FtpFileWrapper ffwrapResult, String remoteResultFile ) throws IOException
       {
          if( ffwrapSource == null || remoteSourceFile == null || ffwrapResult == null || remoteResultFile == null ) { throw new IllegalArgumentException(); }
          ffwrapSource.loginIfNotLoggedIn();
          ffwrapResult.loginIfNotLoggedIn();
          String remoteResultFileTmp = remoteResultFile + TEMP_MARK;
          // Existiert Quelldatei?
          if( !ffwrapSource.existsFile( remoteSourceFile ) ) {
             throw new IllegalArgumentException( "Fehler: Zu kopierende Datei '" + remoteSourceFile + "' existiert nicht." );
          }
          // Loesche beide Zieldateien falls vorhanden:
          if( ffwrapResult.existsFile( remoteResultFile ) && !ffwrapResult.deleteFile( remoteResultFile ) ) {
             throw new IllegalArgumentException( "Fehler: '" + remoteResultFile + "' existiert bereits und kann nicht geloescht werden." );
          }
          if( ffwrapResult.existsFile( remoteResultFileTmp ) && !ffwrapResult.deleteFile( remoteResultFileTmp ) ) {
             throw new IllegalArgumentException( "Fehler: '" + remoteResultFileTmp + "' existiert bereits und kann nicht geloescht werden." );
          }
          // Kopiere in Temp-Datei:
          Exception exception = null;
          boolean ret = false;
          long lenSource1 = ffwrapSource.getLength( remoteSourceFile );
          @SuppressWarnings("resource")
          InputStream is = ffwrapSource.retrieveBinaryFile( remoteSourceFile );
          if( is == null ) {
             throw new IOException( "Fehler: Copy von '" + remoteSourceFile + "' fehlgeschlagen: InputStream ist null." );
          }
          try {
             ret = ffwrapResult.storeFile( is, remoteResultFileTmp );
          } catch( Exception ex ) {
             exception = ex;
          } finally {
             is.close();
             ffwrapSource.completePendingCommand();
          }
          // Pruefe Dateigroessen:
          long lenSource2 = 0;
          long lenResult1 = 0;
          if( exception == null && ret ) {
             lenSource2 = ffwrapSource.getLength( remoteSourceFile );
             lenResult1 = ffwrapResult.getLength( remoteResultFileTmp );
          }
          if( exception != null || !ret || lenSource1 != lenSource2 || lenSource1 != lenResult1 ) {
             try { if( ffwrapResult.existsFile( remoteResultFileTmp ) ) { ffwrapResult.deleteFile( remoteResultFileTmp ); } } catch( IOException e ) {/*ok*/}
          }
          if( exception != null ) {
             throw new IOException( "Fehler: Copy von '" + remoteSourceFile + "' nach '" + remoteResultFileTmp + "' fehlgeschlagen. ", exception );
          }
          if( !ret ) {
             throw new IOException( "Fehler: Copy von '" + remoteSourceFile + "' nach '" + remoteResultFileTmp + "' fehlgeschlagen." );
          }
          if( lenSource1 != lenSource2 ) {
             throw new IOException( "Fehler: Copy fehlgeschlagen: Die Dateigroesse der Sourcedatei '" + remoteSourceFile +
                   "' hat sich waehrend des Kopierens geaendert von " + lenSource1 + " Byte zu " + lenSource2 + " Byte." );
          }
          if( lenSource1 != lenResult1 ) {
             throw new IOException( "Fehler: Copy fehlgeschlagen: Dateigroesse (" + lenResult1 + " Byte) von resultierender Datei '" +
                   remoteResultFileTmp + "' anders als von Sourcedatei '" + remoteSourceFile + "' (" + lenSource1 + " Byte)." );
          }
          // Rename von Temp- zu Zieldatei:
          try {
             ret &= ffwrapResult.renameFile( remoteResultFileTmp, remoteResultFile );
          } catch( Exception ex ) {
             exception = ex;
          }
          long lenResult2 = 0;
          if( exception == null && ret ) {
             lenResult2 = ffwrapResult.getLength( remoteResultFile );
          }
          if( exception != null || !ret || lenSource1 != lenResult2 ) {
             try { if( ffwrapResult.existsFile( remoteResultFile ) ) { ffwrapResult.deleteFile( remoteResultFile ); } } catch( IOException e ) {/*ok*/}
          }
          if( exception != null ) {
             throw new IOException( "Fehler: Copy/Rename von '" + remoteResultFileTmp + "' nach '" + remoteResultFile + "' fehlgeschlagen. ", exception );
          }
          if( !ret ) {
             throw new IOException( "Fehler: Copy/Rename von '" + remoteResultFileTmp + "' nach '" + remoteResultFile + "' fehlgeschlagen." );
          }
          if( lenSource1 != lenResult2 ) {
             throw new IOException( "Fehler: Copy/Rename fehlgeschlagen: Dateigroesse (" + lenResult2 + " Byte) von resultierender Datei '" +
                                    remoteResultFile + "' anders als von Sourcedatei '" + remoteSourceFile + "' (" + lenSource1 + " Byte)." );
          }
       }
    
       /**
        * Verschiebe Remote-Datei.
        * @param  remoteSourceFile Remote-Quell-Datei
        * @param  remoteResultFile Remote-Ergebnis-Datei
        * @return true falls erfolgreich
        */
       public boolean moveRemoteFile( String remoteSourceFile, String remoteResultFile ) throws IOException
       {
          if( remoteSourceFile == null || remoteResultFile == null ) { throw new IllegalArgumentException(); }
          if( remoteSourceFile.equals( remoteResultFile ) ) { return false; }
          if( copyRemoteFile( remoteSourceFile, remoteResultFile ) ) {
             return deleteFile( remoteSourceFile );
          }
          return false;
       }
    
       /**
        * Verschiebe Remote-Datei.
        * Zu den beiden FtpFileWrapper erfolgt kein logout(), dieser muss extern erfolgen.
        * @param  ffwrapSource     FtpFileWrapper fuer Remote-Quell-Datei
        * @param  remoteSourceFile Remote-Quell-Datei
        * @param  ffwrapResult     FtpFileWrapper fuer Remote-Ergebnis-Datei
        * @param  remoteResultFile Remote-Ergebnis-Datei
        * @return true falls erfolgreich
        */
       public static boolean moveRemoteFile( FtpFileWrapper ffwrapSource, String remoteSourceFile, FtpFileWrapper ffwrapResult, String remoteResultFile ) throws IOException
       {
          if( remoteSourceFile == null || remoteResultFile == null || ffwrapSource == ffwrapResult ) { throw new IllegalArgumentException(); }
          if( copyRemoteFile( ffwrapSource, remoteSourceFile, ffwrapResult, remoteResultFile ) ) {
             return ffwrapSource.deleteFile( remoteSourceFile );
          }
          return false;
       }
    
       /**
        * Verschiebe Remote-Dateien.
        * @param  remoteSourceDir         Remote-Quell-Verzeichnis
        * @param  remoteResultDir         Remote-Ergebnis-Verzeichnis
        * @param  filenames               Liste der zu verschiebenen Dateien
        * @param  minZeitProDateiMillisek Minimale Zeit in Millisekunden pro verschobener Datei, z.B. 120 ms.
        */
       public void moveRemoteFiles( String remoteSourceDir, String remoteResultDir, List<String> filenames, int minZeitProDateiMillisek ) throws IOException
       {
          if( filenames == null || remoteSourceDir == null || remoteResultDir == null || remoteSourceDir.equals( remoteResultDir ) ) { throw new IllegalArgumentException(); }
          if( filenames.isEmpty() ) { return; }
          FtpFileWrapper ffwrapSource = new FtpFileWrapper( ftpOderFileProps );
          FtpFileWrapper ffwrapResult = new FtpFileWrapper( ftpOderFileProps );
          ffwrapSource.login();
          try {
             ffwrapResult.login();
             try {
                String wd = getWorkingDirectory();
                if( wd != null && wd.trim().length() > 0 && !"/".equals( wd ) ) {
                   ffwrapSource.changeWorkingDirectory( wd );
                   ffwrapResult.changeWorkingDirectory( wd );
                }
                for( String filename : filenames ) {
                   boolean   ok = false;
                   Exception ex = null;
                   long startZeitNanosek = System.nanoTime();
                   try {
                      ok = moveRemoteFile( ffwrapSource, remoteSourceDir + "/" + filename, ffwrapResult, remoteResultDir + "/" + filename );
                   } catch( Exception e ) {
                      ex = e;
                   }
                   if( !ok || ex != null ) {
                      throw new IOException( "Fehler: '" + filename + "' konnte nicht von '" + remoteSourceDir + "' nach '" + remoteResultDir + "' verschoben werden.", ex );
                   }
                   long verbrauchteZeitMillisek = (System.nanoTime() - startZeitNanosek) / NANOSEK_PRO_MILLISEK;
                   if( minZeitProDateiMillisek > verbrauchteZeitMillisek ) {
                      try { Thread.sleep( minZeitProDateiMillisek - verbrauchteZeitMillisek ); } catch( InterruptedException e ) {/*ok*/}
                   }
                }
             } finally {
                ffwrapResult.logout();
             }
          } finally {
             ffwrapSource.logout();
          }
       }
    
       /**
        * Lies die ersten Textzeilen aus einer Textdatei in eine Liste von Textzeilen-Strings.<br>
        * Es wird zeilenweise gelesen, bis entweder das Dateiende erreicht ist,
        * oder die angegebene Maximalgroesse ueberschritten ist.
        * @param  remoteSourceFile Textdatei, aus der gelesen wird
        * @param  encoding         Encoding, z.B. ISO-8859-1 oder UTF-8
        * @param  maxLength        Die Anzahl der gelesenen Zeichen ueberschreitet diese Grenze um hoechstens eine Textzeile
        * @return Liste von Textzeilen-Strings
        */
       public List<String> retrieveTextLines( String remoteSourceFile, String encoding, int maxLength ) throws IOException
       {
          List<String> ss = new ArrayList<String>();
          @SuppressWarnings("resource")
          BufferedReader in = retrieveTextFile( remoteSourceFile, encoding );
          if( in == null ) { return ss; }
          try {
             String line;
             int    len = 0;
             while( len < maxLength && (line = in.readLine()) != null ) {
                len += line.length();
                ss.add( line );
             }
          } finally {
             in.close();
             completePendingCommand();
          }
          return ss;
       }
    
       /**
        * Zeilenweises Lesen aus einer Textdatei.<br>
        * Wichtig: Auf dem returnierten BufferedReader muss die close()-Methode aufgerufen werden
        *          und anschliessend muss completePendingCommand() aufgerufen werden!
        *          Solange der returnierte BufferedReader noch nicht geschlossen wurde,
        *          darf das Verzeichnis nicht gewechselt werden!
        * @param  remoteSourceFile Textdatei, aus der gelesen wird
        * @param  encoding         Encoding, z.B. ISO-8859-1 oder UTF-8
        * @return BufferedReader, ueber den die Textdatei zeilenweise gelesen werden kann
        */
       public BufferedReader retrieveTextFile( String remoteSourceFile, String encoding ) throws IOException
       {
          InputStream is = retrieveBinaryFile( remoteSourceFile );
          return ( is == null ) ? null : new BufferedReader( new InputStreamReader( is, encoding ) );
       }
    
       /**
        * Binaeres Lesen aus einer Datei.<br>
        * Wichtig: Auf dem returnierten InputStream muss die close()-Methode aufgerufen werden
        *          und anschliessend muss completePendingCommand() aufgerufen werden!
        *          Solange der returnierte InputStream noch nicht geschlossen wurde,
        *          darf das Verzeichnis nicht gewechselt werden!
        * @param  remoteSourceFile Datei, aus der gelesen wird
        * @return InputStream ueber den die Datei gelesen werden kann
        */
       public InputStream retrieveBinaryFile( String remoteSourceFile ) throws IOException
       {
          registerTasktimeAndDelay();
          loginIfNotLoggedIn();
          if( remoteSourceFile == null || remoteSourceFile.trim().length() == 0 ) { throw new IllegalArgumentException(); }
          if( ftpOrFile ) {
             // FTP:
             String fl = remoteSourceFile.trim().replace( '\\', '/' );
             if( !existsFileWithLoop( fl ) ) {
                LOGGER.error( "FTP-Fehler: '" + fl + "' existiert nicht." );
                throw new FileNotFoundException( "Fehler: '" + fl + "' existiert nicht." );
             }
             return ftpClient.retrieveFileStream( fl );
          }
          // Dateisystem:
          return new FileInputStream( getFsRootActPath( remoteSourceFile ) + remoteSourceFile.trim() );
       }
    
       /**
        * Speichere InputStream in Remote-Datei.<br>
        * Wichtig: Der InputStream wird nicht innerhalb der Methode geschlossen und muss ausserhalb geschlossen werden.
        * @param  is               InputStream aus dem die Daten gelesen werden
        * @param  remoteResultFile Remote-Datei in die geschrieben wird
        * @return true falls erfolgreich
        */
       public boolean storeFile( InputStream is, String remoteResultFile ) throws IOException
       {
          return storeFile( is, remoteResultFile, false );
       }
    
       /**
        * Speichere InputStream in Remote-Datei.<br>
        * Wichtig: Der InputStream wird nicht innerhalb der Methode geschlossen und muss ausserhalb geschlossen werden.
        * @param  is               InputStream aus dem die Daten gelesen werden
        * @param  remoteResultFile Remote-Datei in die geschrieben wird
        * @param  append           Anhaengen an existierende Datei falls true
        * @return true falls erfolgreich
        */
       public boolean storeFile( InputStream is, String remoteResultFile, boolean append ) throws IOException
       {
          registerTasktimeAndDelay();
          loginIfNotLoggedIn();
          if( is == null || remoteResultFile == null || remoteResultFile.trim().length() == 0 ) {
             throw new IllegalArgumentException();
          }
          if( !append && existsFile( remoteResultFile ) && !deleteFile( remoteResultFile ) ) {
             throw new IllegalArgumentException( "Fehler: '" + remoteResultFile + "' existiert bereits und kann nicht geloescht werden." );
          }
          if( ftpOrFile ) {
             // FTP:
             String fl = remoteResultFile.trim().replace( '\\', '/' );
             String parentPath = (new File( fl )).getParent();
             if( parentPath != null && parentPath.length() > 0 &&
                   !existsDir( parentPath ) && !makeDirectory( parentPath ) ) {
                String s = ftpClient.getReplyString();
                // "!existsDir( parentPath )"-Abfrage doppelt, falls anderer Thread das Verzeichnis angelegt hat:
                if( !existsDir( parentPath ) ) {
                   throw new IOException( "Fehler: Verzeichnis '" + parentPath + "' kann nicht erstellt werden: " + s );
                }
             }
             if( fl.startsWith( "/" ) && ftpRootPath != null && ftpRootPath.length() > 0 ) {
                fl = ftpRootPath + fl;
             }
             boolean ret = false;
             if( append ) {
                ret = ftpClient.appendFile( fl, is );
             } else if( ftpClient.storeFile( fl + TEMP_MARK, is ) ) {
                ret = ftpClient.rename( fl + TEMP_MARK, fl );
             }
             if( ret ) { return ret; }
             throw new IOException( "FTP-storeFile()-Fehler: " + Arrays.asList( ftpClient.getReplyStrings() ) );
          }
          // Dateisystem:
          File   f = new File( remoteResultFile.trim() );
          String parentPath = f.getParent();
          String fileName   = f.getName();
          String completeParentPath = getFsRootActPath( parentPath ) + (( parentPath != null ) ? parentPath : "");
          if( parentPath != null && parentPath.length() > 0 ) {
             File pp = new File( completeParentPath );
             if( !pp.exists() && !pp.mkdirs() ) {
                // "!pp.exists()"-Abfrage doppelt, falls anderer Thread das Verzeichnis angelegt hat:
                if( !pp.exists() ) {
                   return false;
                }
             }
          }
          byte[] buff = new byte[BUFF_SIZE];
          int len;
          String fileNameTemp = ( append ) ? fileName : fileName + TEMP_MARK;
          try( FileOutputStream fos = new FileOutputStream( new File( completeParentPath, fileNameTemp ), append ) ) {
             while( 0 < (len = is.read( buff )) ) {
                fos.write( buff, 0, len );
             }
          }
          if( append ) { return true; }
          return (new File( completeParentPath, fileNameTemp )).renameTo( new File( completeParentPath, fileName ) );
       }
    
       /**
        * Speichere ueber returnierten OutputStream in Remote-Datei.<br>
        * Wichtig: Auf dem returnierten OutputStream muss die close()-Methode aufgerufen werden
        *          und anschliessend muss completePendingCommand() aufgerufen werden!
        *          Solange der returnierte OutputStream noch nicht geschlossen wurde,
        *          darf das Verzeichnis nicht gewechselt werden!<br>
        *          Ausserdem sollte ueberlegt werden, zuerst in einen temporaeren Dateinamen zu speichern
        *          und erst nach vollendetem Speichern ein Rename zum endgueltigen Dateinamen durchzufuehren,
        *          um das Risiko von durch andere Prozesse nur teilweise kopierten Dateien zu reduzieren.
        * @param  remoteResultFile Remote-Datei in die geschrieben wird
        * @return der OutputStream in den geschrieben wird
        */
       public OutputStream storeFile( String remoteResultFile ) throws IOException
       {
          return storeFile( remoteResultFile, false );
       }
    
       /**
        * Speichere ueber returnierten OutputStream in Remote-Datei.<br>
        * Wichtig: Auf dem returnierten OutputStream muss die close()-Methode aufgerufen werden
        *          und anschliessend muss completePendingCommand() aufgerufen werden!
        *          Solange der returnierte OutputStream noch nicht geschlossen wurde,
        *          darf das Verzeichnis nicht gewechselt werden!<br>
        *          Ausserdem sollte ueberlegt werden, zuerst in einen temporaeren Dateinamen zu speichern
        *          und erst nach vollendetem Speichern ein Rename zum endgueltigen Dateinamen durchzufuehren,
        *          um das Risiko von durch andere Prozesse nur teilweise kopierten Dateien zu reduzieren.
        * @param  remoteResultFile Remote-Datei in die geschrieben wird
        * @param  append           Anhaengen an existierende Datei falls true
        * @return der OutputStream in den geschrieben wird
        */
       @SuppressWarnings("resource")
       public OutputStream storeFile( String remoteResultFile, boolean append ) throws IOException
       {
          registerTasktimeAndDelay();
          loginIfNotLoggedIn();
          if( remoteResultFile == null || remoteResultFile.trim().length() == 0 ) { throw new IllegalArgumentException(); }
          if( remoteResultFile.contains( "/" ) || remoteResultFile.contains( "\\" ) ) {
             throw new IllegalArgumentException( "Kein Unterverzeichnis erlaubt in '" + remoteResultFile + "'." );
          }
          if( ftpOrFile ) {
             String fl = remoteResultFile.trim().replace( '\\', '/' );
             String parentPath = (new File( fl )).getParent();
             if( parentPath != null && parentPath.length() > 0 &&
                   !existsDir( parentPath ) && !makeDirectory( parentPath ) ) {
                String s = ftpClient.getReplyString();
                // "!existsDir( parentPath )"-Abfrage doppelt, falls anderer Thread das Verzeichnis angelegt hat:
                if( !existsDir( parentPath ) ) {
                   throw new IOException( "Fehler: Verzeichnis '" + parentPath + "' kann nicht erstellt werden: " + s );
                }
             }
             OutputStream os = null;
             if( append ) {
                os = ftpClient.appendFileStream( fl );
             } else {
                os = ftpClient.storeFileStream( fl );
             }
             if( os == null ) {
                String dir = getWorkingDirectory();
                dir = ( dir == null || dir.length() == 0 ) ? "" : ( dir.endsWith( "/" ) ) ? dir : (dir + "/");
                throw new IOException( "Fehler: In die Datei '" + dir + remoteResultFile + "' kann nicht geschrieben werden. " );
             }
             return os;
          }
          return new FileOutputStream( new File( getFsRootActPath( remoteResultFile ), remoteResultFile ), append );
       }
    
       /**
        * Konvertiere die Separator-Zeichen in einem Pfad-String zu zum jeweiligen System passenden Zeichen.
        * @param  path Input-Pfad-String
        * @return Ergebnis-Pfad-String
        */
       public String convertPathToSystemDependendPath( String path )
       {
          if( path == null ) { return null; }
          if( ftpOrFile ) { return path.replace( '\\', '/' ); }
          return path.replace( ( File.separatorChar == '\\' ) ? '/' : '\\', File.separatorChar );
       }
    
       /**
        * Properties aus Properties-Datei in Properties-Objekt laden.<br>
        * Die Properties-Datei kann entweder im CLASSPATH liegen oder per Pfad erreicht werden.
        * @param  propFile Dateiname der Properties-Datei
        * @return Properties-Objekt
        */
       public static Properties loadPropertiesFromFileFromDirOrClasspath( String propFile )
       {
          Properties props = new Properties();
          if( propFile == null || propFile.trim().length() == 0 ) { return props; }
    
          // Properties aus Datei vom CLASSPATH einlesen:
          try {
             String propFileCp = ( propFile.startsWith( "/" ) || propFile.startsWith( "\\" ) )
                                 ? propFile : (File.separator + propFile);
             @SuppressWarnings("resource")
             InputStream is = ClassLoader.getSystemResourceAsStream( propFileCp );
             if( is != null ) {
                try { props.load( is ); } finally { is.close(); }
             }
          } catch( IOException e ) {/* nothing to do */}
    
          // Properties aus Datei per Pfad einlesen:
          if( props.isEmpty() ) {
             try( InputStream is = new FileInputStream( propFile ) ) {
                props.load( is );
             } catch( IOException e ) {/* nothing to do */}
          }
          return props;
       }
    
       /**
        * Extrahiere FTP-Parameter aus FTP-URI.
        * @param  uri FTP-URI, muss mit "ftp://" beginnen, z.B.: ftp://usr:pwd@host:21/path
        * @return Properties mit FTP-Parametern
        */
       public static Properties extractFtpParmsFromUri( String uri )
       {
          if( uri == null || uri.trim().length() <= FTP_PROT_URI.length() ||
                            !uri.trim().toLowerCase( Locale.GERMAN ).startsWith( FTP_PROT_URI ) ) {
             return null;
          }
          URI u;
          try {
             u = new URI( uri.trim() );
          } catch( URISyntaxException ex ) {
             throw new IllegalArgumentException( "Fehler: Ungueltige URI '" + uri + "': " + ex );
          }
          Properties p  = new Properties();
          String usrPwd = u.getUserInfo();
          String host   = u.getHost();
          int    port   = u.getPort();
          String path   = u.getPath();
          if( host == null || host.trim().length() == 0 ) { return null; }
          p.put( FTP_HOST_KEY, host );
          p.put( FTP_PORT_KEY, ( port > 0 ) ? (port + "") : "21" );
          if( path.length() > 0 ) { p.put( FTP_PATH_KEY, path ); }
          if( usrPwd != null && usrPwd.length() > 0 ) {
             String usr, pwd;
             int pos = usrPwd.indexOf( ':' );
             if( pos >= 0 ) {
                usr = usrPwd.substring( 0, pos ).trim();
                pwd = usrPwd.substring( pos + 1 ).trim();
             } else {
                usr = usrPwd.trim();
                pwd = "";
             }
             if( usr.length() > 0 ) { p.put( FTP_USR_KEY, usr ); }
             if( pwd.length() > 0 ) { p.put( FTP_PWD_KEY, pwd ); }
          }
          return p;
       }
    
       /**
        * Returniere Properties-Werte als Text-String, dabei wird das FTP-Passwort durch drei Punkte ersetzt.
        * @return Text-String der Properties-Werte, aber ohne FTP-Passwort
        */
       public String getPropsAsString()
       {
          return getPropsAsString( ftpOderFileProps );
       }
    
       /**
        * Returniere Properties-Werte als Text-String, dabei wird das FTP-Passwort durch drei Punkte ersetzt.
        * @param  p Properties
        * @return Text-String der Properties-Werte, aber ohne FTP-Passwort
        */
       public static String getPropsAsString( Properties p )
       {
          if( p == null ) { return null; }
          Properties propsOhnePwd = new Properties();
          propsOhnePwd.putAll( p );
          if( propsOhnePwd.get( FtpFileWrapper.FTP_PWD_KEY ) != null ) { propsOhnePwd.put( FtpFileWrapper.FTP_PWD_KEY, "..." ); }
          if( propsOhnePwd.get( FtpFileWrapper.FTP_URI_KEY ) != null ) {
             String s = propsOhnePwd.getProperty( FtpFileWrapper.FTP_URI_KEY );
             int pos2 = s.indexOf( '@' );
             int pos1 = ( pos2 > 0 ) ? s.lastIndexOf( ':', pos2 ) : -1;
             if( pos1 > 0 ) {
                propsOhnePwd.put( FtpFileWrapper.FTP_URI_KEY, s.substring( 0, pos1 + 1 ) + "..." + s.substring( pos2 ) );
             }
          }
          return propsOhnePwd.toString();
       }
    
       /**
        * Returniere Anzahl der Logins.
        * @return loginCount
        */
       public static long getLoginCount()
       {
          return LOGIN_COUNT.get();
       }
    
       /**
        * Zip: Einen InputStream in einen OutputStream zippen.
        * Wichtig: Auf beide Streams muss die close()-Methode aufgerufen werden.
        * @param instreamForZip  InputStream, der gezipped werden soll
        * @param outstreamZipped OutputStream, gezipped
        * @param origFilename    Originaldateiname (wird in der Zipdatei gespeichert)
        */
       public static void zipStream( InputStream instreamForZip, OutputStream outstreamZipped, String origFilename ) throws IOException
       {
          byte[] buf = new byte[65536];
          int len;
          ZipOutputStream zout = new ZipOutputStream( new BufferedOutputStream( outstreamZipped ) );
          zout.putNextEntry( new ZipEntry( origFilename ) );
          while( (len = instreamForZip.read( buf )) > 0 ) {
             zout.write( buf, 0, len );
          }
          zout.closeEntry();
          zout.finish();
          zout.flush();
       }
    
       /**
        * Unzip: Beim Entzippen von Dateien, die sehr viele Teildateien enthalten, kann es leicht passieren,
        * dass nicht genuegend freie Ports vorhanden sind.<br>
        * Pro zu entzippender Teildatei wird ein Port benoetigt, der fuer circa 2 Minuten blockiert bleibt.<br>
        * Deshalb bietet diese Methode die Moeglichkeit, eine Zeitverzoegerung pro zu entzippender Teildatei anzugeben.
        * @param  ffwrap                    FtpFileWrapper fuer den FTP-Zugang.
        * @param  remoteSourceZipFile       Zu entzippende Quell-Zipdatei.
        * @param  remoteDestDir             FTP-Zielverzeichnis.
        * @param  remoteFirstResultFilename falls null: der im Zip eingetragene Dateinamen, sonst erhaelt erste Datei diesen Namen
        * @param  minZeitProDateiMillisek   Minimale Zeit in Millisekunden pro entzippter Teildatei, z.B. 120 ms.
        * @return Anzahl entzippter Teildateien.
        */
       public static long unzipRemoteVerzoegert( FtpFileWrapper ffwrap, String remoteSourceZipFile, String remoteDestDir, String remoteFirstResultFilename, int minZeitProDateiMillisek ) throws IOException
       {
          Exception      ex = null;
          String         remoteResultFilename = null;
          long           anzahlEntries = 0;
          String         destDir       = ( remoteDestDir == null || remoteDestDir.trim().length() == 0 ) ? "" :
                                         ( remoteDestDir.trim().endsWith( "/" ) ) ? remoteDestDir.trim() : (remoteDestDir.trim() + "/");
          FtpFileWrapper ffwrapDest    = ffwrap.createSecondFtpFileWrapper();
          ffwrapDest.login();
          try {
             if( destDir != null && destDir.length() > 0 ) {
                ffwrapDest.changeWorkingDirectory( destDir );
             }
             try( InputStream instreamZipped = ffwrap.retrieveBinaryFile( remoteSourceZipFile ) ) {
                try( ZipInputStream zin = new ZipInputStream( new BufferedInputStream( instreamZipped ) ) ) {
                   ZipEntry zipEntry;
                   while( (zipEntry = zin.getNextEntry()) != null ) {
                      long startZeitNanosek = System.nanoTime();
                      remoteResultFilename = zipEntry.getName();
                      if( anzahlEntries == 0 && remoteFirstResultFilename != null && remoteFirstResultFilename.trim().length() > 0 ) {
                         remoteResultFilename = remoteFirstResultFilename;
                      }
                      if( remoteResultFilename != null && remoteResultFilename.startsWith( "/" ) && remoteResultFilename.length() > 1 ) {
                         remoteResultFilename = remoteResultFilename.substring( 1 );
                      }
                      try( BufferedOutputStream os = new BufferedOutputStream( ffwrapDest.storeFile( remoteResultFilename ) ) ) {
                         int size;
                         byte[] buffer = new byte[64 * 1024];
                         while( (size = zin.read( buffer, 0, buffer.length )) > 0 ) {
                            os.write( buffer, 0, size );
                         }
                      } catch( Exception e ) {
                         ex = e;
                      } finally {
                         ffwrapDest.completePendingCommand();
                      }
                      zin.closeEntry();
                      anzahlEntries++;
                      long verbrauchteZeitMillisek = (System.nanoTime() - startZeitNanosek) / NANOSEK_PRO_MILLISEK;
                      if( minZeitProDateiMillisek > verbrauchteZeitMillisek ) {
                         try { Thread.sleep( minZeitProDateiMillisek - verbrauchteZeitMillisek ); } catch( InterruptedException e ) {/*ok*/}
                      }
                   }
                } catch( Exception e ) {
                   if( ex == null ) { ex = e; }
                }
             } finally {
                ffwrap.completePendingCommand();
             }
          } finally {
             ffwrapDest.logout();
          }
          if( ex != null ) { throw new IOException( "Fehler beim Unzip von " + remoteSourceZipFile + ", letzter Zip-Entry " + remoteResultFilename + ",", ex ); }
          return anzahlEntries;
       }
    
       /**
        * Liste von Dateidaten rekursiv im Verzeichnis und in allen Unterverzeichnissen.
        * @param  startDir                 Startverzeichnis
        * @param  regexAllowedCompleteDirs Regex-Ausdruck fuer zu verwendende komplette Unterverzeichnispfade (also inkl. aller Unterverzeichnisse)
        * @param  ignoreSingleDirs         zu ignorierende Unterverzeichnisse, aber nicht komplette Unterverzeichnispfade, sondern einzelne Unterverzeichnisse
        * @return Liste von FtpFsFileData-Objekten (mit den Infos zu den Dateien)
        */
       public List<FtpFsFileData> listFtpFsFileDataRecursive( String startDir, String regexAllowedCompleteDirs, Set<String> ignoreSingleDirs )
       {
          loginIfNotLoggedInISE();
          List<FtpFsFileData> fds = listFtpFsFileData( startDir, false );
          if( fds == null ) { return null; }
          String prefixDir = ( startDir == null || startDir.trim().length() == 0 ) ? "" : (( startDir.trim().endsWith( "/" ) ) ? startDir.trim() : startDir.trim() + "/");
          Pattern regexAllowedCompleteDirsPattern = ( regexAllowedCompleteDirs != null && regexAllowedCompleteDirs.trim().length() > 0 ) ? Pattern.compile( regexAllowedCompleteDirs.trim() ) : null;
          List<FtpFsFileData> fdsResult = new ArrayList<FtpFsFileData>();
          for( FtpFsFileData fd : fds ) {
             if( fd.isFile ) {
                fdsResult.add( fd );
             } else if( fd.isDirectory ) {
                if( (regexAllowedCompleteDirsPattern == null || regexAllowedCompleteDirsPattern.matcher( prefixDir + fd.name ).matches()) &&
                    (ignoreSingleDirs == null || !ignoreSingleDirs.contains( fd.name )) ) {
                   fdsResult.addAll( listFtpFsFileDataRecursive( prefixDir + fd.name, regexAllowedCompleteDirs, ignoreSingleDirs ) );
                }
             }
          }
          return fdsResult;
       }
    
       /**
        * Liste Daten zu Dateien und Verzeichnissen.
        */
       public List<FtpFsFileData> listFtpFsFileData( String subDir )
       {
          return listFtpFsFileData( subDir, false );
       }
    
       /**
        * Liste Daten zu Dateien und Verzeichnissen.
        * Zu MLSD siehe {@link #listFtpFiles( String subDir, boolean withMLSD )}.
        */
       private List<FtpFsFileData> listFtpFsFileData( String subDir, boolean withMLSD )
       {
          loginIfNotLoggedInISE();
          List<FtpFsFileData> fds = new ArrayList<FtpFsFileData>();
          if( ftpOrFile ) {
             try {
                FTPFile[] ftpFiles = listFtpFiles( subDir, withMLSD );
                if( ftpFiles != null ) {
                   for( FTPFile f : ftpFiles ) {
                      FtpFsFileData fd = new FtpFsFileData();
                      fd.timestamp   = f.getTimestamp();
                      fd.size        = f.getSize();
                      fd.name        = f.getName();
                      fd.isFile      = f.isFile();
                      fd.isDirectory = f.isDirectory();
                      fd.parentPath  = subDir;
                      fds.add( fd );
                   }
                }
             } catch( IOException e ) {/* nothing to do*/}
          } else {
             File[] files = listFsFiles( subDir );
             if( files != null ) {
                for( File f : files ) {
                   FtpFsFileData fd = new FtpFsFileData();
                   Calendar cal = Calendar.getInstance();
                   cal.setTimeInMillis( f.lastModified() );
                   fd.timestamp   = cal;
                   fd.size        = f.length();
                   fd.name        = f.getName();
                   fd.isFile      = f.isFile();
                   fd.isDirectory = f.isDirectory();
                   fd.parentPath  = subDir;
                   fds.add( fd );
                }
             }
          }
          return fds;
       }
    
       /**
        * Liste Dateien und Verzeichnisse.
        * Nur Dateisystem, nicht FTP.
        */
       private File[] listFsFiles( String subDir )
       {
          String verz = ( subDir == null || subDir.trim().length() == 0 ) ? "" : subDir.trim();
          return (new File( getFsRootActPath( verz ) + verz )).listFiles();
       }
    
       /**
        * Liste Dateien und Verzeichnisse.
        * Nur FTP, nicht Dateisystem.
        * Ohne MLSD wird der Zeitstempel nicht immer korrekt ausgegeben, allerdings funktioniert MLSD nicht auf allen FTP-Servern.
        * Zu MLSD siehe {@link "http://tools.ietf.org/html/rfc3659"}.
        * @param subDir   Unterverzeichnis
        * @param withMLSD true falls MLSD verwendet werden soll
        * @return Array von FTPFile-Objekten
        */
       private FTPFile[] listFtpFiles( String subDir, boolean withMLSD ) throws IOException
       {
          FTPFile[] ftpFiles = null;
          if( subDir == null || subDir.trim().length() == 0 ) {
             ftpFiles = ( withMLSD ) ? ftpClient.mlistDir() : ftpClient.listFiles();
          } else {
             // Mit AS/400-FTP-Server ("OS/400", "V5R4M0") funktioniert "ftpClient.listFiles( subDir )" nicht:
             String oldWorkDir = getWorkingDirectory();
             try {
                if( changeWorkingDirectory( subDir ) ) {
                   ftpFiles = ( withMLSD ) ? ftpClient.mlistDir() : ftpClient.listFiles();
                }
             } finally {
                if( !changeWorkingDirectory( oldWorkDir ) ) {
                   throw new IOException( "Fehler: " + Arrays.asList( ftpClient.getReplyStrings() ) );
                }
             }
          }
          return ftpFiles;
       }
    
       /**
        * Ermittle Dateiinformationen zu einer Datei
        * @return Dateiinformationen
        */
       public FtpFsFileData getFtpFsFileData( String file )
       {
          FtpFsFileData fd = new FtpFsFileData();
          if( file == null || file.trim().length() == 0 ) {
             return fd;
          }
          loginIfNotLoggedInISE();
          if( ftpOrFile ) {
             File f = new File( file );
             String parentPath = f.getParent();
             String fileName   = f.getName();
             if( fileName == null || fileName.length() == 0 ) { return fd; }
             try {
                // Mit AS/400-FTP-Server ("OS/400", "V5R4M0") funktioniert "ftpClient.listFiles( subDir/fileName )" nicht:
                String oldWorkDir = getWorkingDirectory();
                try {
                   if( changeWorkingDirectory( parentPath ) ) {
                      FTPFile[] ftpFiles = ftpClient.listFiles( fileName );
                      if( ftpFiles != null && ftpFiles.length == 1 ) {
                         fd.timestamp   = ftpFiles[0].getTimestamp();
                         fd.size        = ftpFiles[0].getSize();
                         fd.name        = ftpFiles[0].getName();
                         fd.isFile      = ftpFiles[0].isFile();
                         fd.isDirectory = ftpFiles[0].isDirectory();
                         fd.parentPath  = parentPath;
                      }
                   }
                } finally {
                   if( !changeWorkingDirectory( oldWorkDir ) ) {
                      throw new IOException( "Fehler: " + Arrays.asList( ftpClient.getReplyStrings() ) );
                   }
                }
             } catch( IOException e ) {/* nothing to do*/}
          } else {
             File f = new File( getFsRootActPath( file ) + file );
             if( f.exists() ) {
                Calendar cal = Calendar.getInstance();
                cal.setTimeInMillis( f.lastModified() );
                fd.timestamp   = cal;
                fd.size        = f.length();
                fd.name        = f.getName();
                fd.isFile      = f.isFile();
                fd.isDirectory = f.isDirectory();
                fd.parentPath  = (new File( file )).getParent();
             }
          }
          return fd;
       }
    
       /** 
        * Ermittlung der FTP-Server-Software.
        * "ftpClient.getSystemType()" liefert beim FTP-Server "vsFTPd 2.2.2" nur: "UNIX Type: L8".
        * Deshalb wird zusaetzlich "ftpClient.getStatus()" verwendet.
       */
       public String ermittleFtpServerSoftware() throws IOException
       {
          if( !ftpOrFile ) {
             return "Fileserver";
          }
          String systType = ftpClient.getSystemType();
          if( systType != null ) {
             systType = systType.trim();
             String systTypeLowcase = systType.toLowerCase();
             if( systTypeLowcase.contains( "apache ftpserver" ) ||
                 systTypeLowcase.contains( "proftpd" ) ||
                 systTypeLowcase.contains( "vsftpd"  ) ||
                 systTypeLowcase.contains( "os/400"  ) ) {
                if( systTypeLowcase.startsWith( "unix type:" ) && systType.length() > 12 ) {
                   return systType.substring( 10 ).trim();
                }
                if( systType.length() > 2 ) {
                   return systType;
                }
             }
          }
          String ftpServerName = null;
          String stat = ftpClient.getStatus();
          String[] ss = stat.split( "\n" );
          if( ss.length > 0 ) {
             String ss0 = ss[0].trim();
             if( ss0.startsWith( "211"       ) ) { ss0 = ss0.substring( 3 ).trim(); }
             if( ss0.startsWith( "-"         ) ) { ss0 = ss0.substring( 1 ).trim(); }
             if( ss0.startsWith( "Status of" ) ) { ss0 = ss0.substring( 9 ).trim(); }
             if( ss0.endsWith(   ":"         ) ) { ss0 = ss0.substring( 0, ss0.length() - 1 ).trim(); }
             if( !"FTP server status".equalsIgnoreCase( ss0 ) && !ss0.contains( "connected" ) ) {
                ftpServerName = ss0;
             }
             for( int i = 1; i < ss.length; i++ ) {
                if( ss[i].toLowerCase().contains( "vsftpd" ) ) {
                   ftpServerName = ss[i].trim();
                }
             }
          }
          if( ftpServerName != null && ftpServerName.endsWith( "- secure, fast, stable" ) ) {
             ftpServerName = ftpServerName.substring( 0, ftpServerName.length() - 22 ).trim();
          }
          if( ftpServerName != null && ftpServerName.length() > 2 ) {
             return ftpServerName;
          }
          return systType;
       }
    
       /**
        * Nur im Fileserver-Betrieb:
        * @param  pathOrFile
        * @return Ermittle den voranzustellenden internen Pfad
        */
       private String getFsRootActPath( String pathOrFile )
       {
          if( pathOrFile != null && (pathOrFile.trim().startsWith( "/" ) || pathOrFile.trim().startsWith( "\\" )) ) {
             return fsRootDir;
          }
          return fsRootDir + fsActSubDir + File.separator;
       }
    
       /**
        * Fuehre login() aus, falls noch nicht eingeloggt.
        * @return true falls erfolgreich
        * @throws IllegalStateException statt IOException
        */
       private boolean loginIfNotLoggedInISE()
       {
          try {
             if( loginIfNotLoggedIn() ) { return loggedin; }
             throw new IllegalStateException( createLoginErrMsg() );
          } catch( IOException ex ) {
             throw new IllegalStateException( ex );
          }
       }
    
       /**
        * Ermittle, ob schon oder noch eingeloggt.
        * @return true falls eingeloggt
        */
       private boolean isLoggedIn()
       {
          if( !loggedin || !ftpOrFile ) {
             return loggedin;
          }
          if( ftpClient == null ) {
             return false;
          }
          try {
             ftpClient.printWorkingDirectory();
             return loggedin;
          } catch( IOException e ) {
             return false;
          }
       }
    
       /**
        * Fuehre login() aus, falls noch nicht eingeloggt.
        * @return true falls erfolgreich
        */
       private boolean loginIfNotLoggedIn() throws IOException
       {
          loggedin = isLoggedIn();
          if( loggedin ) {
             return loggedin;
          }
          try {
             String actSubDir = ftpActSubDir;
             if( login() ) {
                if( ftpOrFile && actSubDir != null && actSubDir.length() > 0 && !"/".equals( actSubDir ) ) {
                   if( !ftpClient.changeWorkingDirectory( actSubDir ) ) {
                      throw new IOException( "FTP-Fehler mit FTP-Path '" + actSubDir + "': " + Arrays.asList( ftpClient.getReplyStrings() ) );
                   }
                }
                return loggedin;
             }
             throw new IOException( createLoginErrMsg() );
          } catch( IOException ex ) {
             throw new IOException( createLoginErrMsg(), ex );
          }
       }
    
       /**
        * Returniere Login-Fehlermeldung inklusive der Properties-Werte als Text-String.
        * @return Login-Fehlermeldung
        */
       private String createLoginErrMsg()
       {
          return "Fehler: Kein FtpFileWrapper-Login moeglich, uebergebene Properties: " + getPropsAsString() + ".";
       }
    
       /**
        * Pruefe ob String nicht null ist, nicht leer ist und nicht nur aus Leerzeichen besteht.
        */
       private static boolean checkStringNichtLeer( String s )
       {
          return s != null && s.trim().length() > 0;
       }
    
       /**
        * finalize().
        */
       @Override
       protected void finalize() throws Throwable
       {
          logout();
          super.finalize();
       }
    
       public static class FtpFsFileData
       {
          public boolean  isFile;
          public boolean  isDirectory;
          public String   parentPath;
          public String   name;
          public long     size;
          public Calendar timestamp;
       }
    
       static class ConsoleProtocolCommandListener implements ProtocolCommandListener
       {
          @Override public void protocolCommandSent( ProtocolCommandEvent ev ) {
             System.out.print( "Sent:     " + ev.getMessage() );
          }
          @Override public void protocolReplyReceived( ProtocolCommandEvent ev ) {
             System.out.print( "Received: " + ev.getMessage() );
          }
       }
    }
    

    Dieser Wrapper ist als Beispiel zu verstehen. Normalerweise sind Anpassungen an eigene Anforderungen notwendig.

  6. Erstellen Sie im src\test\java\de\meinefirma\meinprojekt\ftp-Verzeichnis die bereits bekannte Klasse: FtpTestUtil.java

    package de.meinefirma.meinprojekt.ftp;
    
    import java.io.*;
    import java.util.*;
    import org.apache.ftpserver.*;
    import org.apache.ftpserver.ftplet.*;
    import org.apache.ftpserver.listener.ListenerFactory;
    import org.apache.ftpserver.usermanager.*;
    import org.apache.ftpserver.usermanager.impl.*;
    
    /**
     * FTP-Test-Utility, basierend auf Apache FtpServer:
     * {@link "http://www.jarvana.com/jarvana/view/org/apache/ftpserver/ftpserver-core/1.0.6/ftpserver-core-1.0.6-javadoc.jar!/org/apache/ftpserver/FtpServer.html"}
     */
    public class FtpTestUtil
    {
       /**
        * Erzeuge FTP-Server.
        * @param ftpPort           FTP-Port, z.B. 2121
        * @param ftpHomeDir        FTP-Verzeichnis, z.B. "target/FtpHome"
        * @param readUserName      leseberechtigter Benutzer: Name
        * @param readUserPwd       leseberechtigter Benutzer: Passwort
        * @param writeUserName     schreibberechtigter Benutzer: Name
        * @param writeUserPwd      schreibberechtigter Benutzer: Passwort
        * @param ftpUsersPropsFile kann null sein, oder z.B. "target/FtpUsers.properties"
        * @param maxLogins         maximale Anzahl von Logins (0 fuer Defaultwert)
        */
       public static FtpServer createFtpServer( int ftpPort, String ftpHomeDir,
             String readUserName, String readUserPwd, String writeUserName, String writeUserPwd ) throws FtpException, IOException
       {
          return createFtpServer( ftpPort, ftpHomeDir, readUserName, readUserPwd, writeUserName, writeUserPwd, null, 0 );
       }
    
       public static FtpServer createFtpServer( int ftpPort, String ftpHomeDir,
             String readUserName, String readUserPwd, String writeUserName, String writeUserPwd,
             String ftpUsersPropsFile, int maxLogins ) throws FtpException, IOException
       {
          return createFtpServer( ftpPort, ftpHomeDir, readUserName, readUserPwd, writeUserName, writeUserPwd,
                                  ftpUsersPropsFile, maxLogins, 0 );
       }
    
       public static FtpServer createFtpServer( int ftpPort, String ftpHomeDir,
             String readUserName, String readUserPwd, String writeUserName, String writeUserPwd,
             String ftpUsersPropsFile, int maxLogins, int maxIdleTimeSec ) throws FtpException, IOException
       {
          File fhd = new File( ftpHomeDir );
          if( !fhd.exists() ) fhd.mkdirs();
    
          ListenerFactory listenerFactory = new ListenerFactory();
          listenerFactory.setPort( ftpPort );
    
          PropertiesUserManagerFactory userManagerFactory = new PropertiesUserManagerFactory();
          userManagerFactory.setPasswordEncryptor( new SaltedPasswordEncryptor() );
          if( ftpUsersPropsFile != null && ftpUsersPropsFile.trim().length() > 0 ) {
             File upf = new File( ftpUsersPropsFile );
             if( !upf.exists() ) upf.createNewFile();
             userManagerFactory.setFile( upf );
          }
    
          // Einen Nur-Lese-User und einen User mit Schreibberechtigung anlegen:
          UserManager userManager = userManagerFactory.createUserManager();
          BaseUser userRd = new BaseUser();
          BaseUser userWr = new BaseUser();
          userRd.setName( readUserName );
          userRd.setPassword( readUserPwd );
          userRd.setHomeDirectory( ftpHomeDir );
          userWr.setName( writeUserName );
          userWr.setPassword( writeUserPwd );
          userWr.setHomeDirectory( ftpHomeDir );
          if( maxIdleTimeSec > 0 ) {
             userRd.setMaxIdleTime( maxIdleTimeSec );
             userWr.setMaxIdleTime( maxIdleTimeSec );
          }
          List<Authority> authorities = new ArrayList<Authority>();
          authorities.add( new WritePermission() );
          userWr.setAuthorities( authorities );
          userManager.save( userRd );
          userManager.save( userWr );
    
          FtpServerFactory serverFactory = new FtpServerFactory();
          serverFactory.addListener( "default", listenerFactory.createListener() );
          serverFactory.setUserManager( userManager );
          if( maxLogins > 0 ) {
             ConnectionConfigFactory ccf = new ConnectionConfigFactory();
             ccf.setMaxLogins( maxLogins );
             serverFactory.setConnectionConfig( ccf.createConnectionConfig() );
          }
          return serverFactory.createServer();
       }
    }
    
  7. Erstellen Sie im src\test\java\de\meinefirma\meinprojekt\ftp-Verzeichnis die Klasse: FtpFileWrapperTest.java

    package de.meinefirma.meinprojekt.ftp;
    
    import java.io.*;
    import java.util.*;
    import org.apache.ftpserver.FtpServer;
    import org.apache.ftpserver.ftplet.FtpException;
    import org.junit.*;
    import de.meinefirma.meinprojekt.ftp.FtpFileWrapper.FtpFsFileData;
    
    public class FtpFileWrapperTest
    {
       private static final int    FTP_PORT        = 2121;
       private static final String FTP_HOST        = "localhost";
       private static final String FTP_PATH        = "ftp-path";
       private static final String FTP_HOME_DIR    = "target/FtpHome";
       private static final String READ_USER_NAME  = "ReadUserName";
       private static final String READ_USER_PWD   = "ReadUserPwd";
       private static final String WRITE_USER_NAME = "WriteUserName";
       private static final String WRITE_USER_PWD  = "WriteUserPwd";
       private static FtpServer ftpServer;
    
       @BeforeClass
       public static void startFtpServer() throws FtpException, IOException
       {
          ftpServer = FtpTestUtil.createFtpServer( FTP_PORT, FTP_HOME_DIR,
                READ_USER_NAME, READ_USER_PWD, WRITE_USER_NAME, WRITE_USER_PWD );
          ftpServer.start();
       }
    
       @AfterClass
       public static void stoppFtpServer()
       {
          // Um den FTP-Server von ausserhalb des Tests eine Zeit lang ueber
          // ftp://WriteUserName:WriteUserPwd@localhost:2121
          // anzusprechen, kann folgende Zeile aktiviert werden:
          // try { Thread.sleep( 55000 ); } catch( InterruptedException e ) {/*ok*/}
          ftpServer.stop();
          ftpServer = null;
       }
    
       /** Teste Fileserver-Variante, Properties-Datei nicht aus CLASSPATH sondern ueber Verzeichnisangabe */
       @Test
       public void testFileserverPropsAusVerzeichnis() throws IOException
       {
          FtpFileWrapper ffwrap = FtpFileWrapper.createFtpFileWrapperFromPropFile( "src/test/resources/FtpFileWrapper-FS.properties" );
          Assert.assertEquals( "{FsRoot=target/FileserverHome}", ffwrap.getPropsAsString() );
          testFtpFileWrapper( ffwrap, false );
       }
    
       /** Teste FTP-Variante, einzelne FTP-Parameter (keine URI), ohne FTP-Context-Path, Properties-Datei aus CLASSPATH */
       @Test
       public void testFtpPropsAusClasspath() throws IOException
       {
          FtpFileWrapper ffwrap = FtpFileWrapper.createFtpFileWrapperFromPropFile( "FtpFileWrapper-FTP.properties" );
          Assert.assertEquals( "{FtpHost=localhost, FtpPort=2121, FtpPwd=..., FtpUsr=WriteUserName}", ffwrap.getPropsAsString() );
          testFtpFileWrapper( ffwrap, true );
       }
    
       /** Teste FTP-Variante mit FTP-URI, ohne FTP-Context-Path, ohne Properties-Datei */
       @Test
       public void testFtpMitUriOhneContextPath() throws IOException
       {
          final String uri = "ftp://" + WRITE_USER_NAME + ":" + WRITE_USER_PWD + "@" + FTP_HOST + ":" + FTP_PORT;
          FtpFileWrapper ffwrap = FtpFileWrapper.createFtpFileWrapperFromUri( uri );
          Assert.assertEquals( "{FtpUri=ftp://WriteUserName:...@localhost:2121}", ffwrap.getPropsAsString() );
          testFtpFileWrapper( ffwrap, true );
       }
    
       /** Teste FTP-Variante mit FTP-URI, mit FTP-Context-Path, ohne Properties-Datei */
       @Test
       public void testFtpMitUriMitContextPath() throws IOException
       {
          final String uri = "ftp://" + WRITE_USER_NAME + ":" + WRITE_USER_PWD + "@" + FTP_HOST + ":" + FTP_PORT + "/" + FTP_PATH;
          FtpFileWrapper ffwrap = FtpFileWrapper.createFtpFileWrapperFromUri( uri );
          Assert.assertEquals( "{FtpUri=ftp://WriteUserName:...@localhost:2121/ftp-path}", ffwrap.getPropsAsString() );
          testFtpFileWrapper( ffwrap, true );
       }
    
       /** Interne gemeinsame FtpFileWrapper-Testmethode sowohl fuer FTP- als auch FS-Betrieb */
       protected static void testFtpFileWrapper( FtpFileWrapper ffwrap, boolean assureFtpOrFile ) throws IOException
       {
          String encoding              = "UTF-8";
          String meinText1             = "BlaBlupp";
          String meinText2             = "äöüß\u20AC"; // \u20AC = Euro-Zeichen
          String meinText3             = "Xyz";
          String meinText              = meinText1 + "\r\n" + meinText2;
          String remoteResultFileName1 = "MeineDatei1.txt";
          String remoteResultFileName2 = "MeineDatei2.txt";
          String remoteResultFileName3 = "MeineDatei3.txt";
          String remoteResultFilePath2 = "meindir2";
          String remoteResultFilePath3 = "meindir3";
          String remoteResultFile2     = remoteResultFilePath2 + "/" + remoteResultFileName2;
          String remoteResultFile3     = remoteResultFilePath3 + "/" + remoteResultFileName3;
          String remotePath4a          = "meindir4";
          String remotePath4b          = "meinsubdir4";
          String remoteResultFile5     = "d1/d2\\d3/r.txt";
          String remoteResultFile6     = "d1\\d2/d3\\f.txt";
          List<String> namesList;
          Assert.assertNotNull( "ffwrap null", ffwrap );
    
          // Test ohne expliziten Login:
          try {
             // FTP-Home-Path anlegen:
             if( assureFtpOrFile && new File( FTP_HOME_DIR ).exists() && !new File( FTP_HOME_DIR + "/" + FTP_PATH ).exists() ) {
                new File( FTP_HOME_DIR + "/" + FTP_PATH ).mkdirs();
             }
    
             // Erst nach Anlegen des FTP-Home-Paths aufrufen:
             Assert.assertEquals( "FtpOrFile", Boolean.valueOf( assureFtpOrFile ),
                                               Boolean.valueOf( ffwrap.getFtpOrFile() ) );
    
             // Schreiben von Remote-Datei ohne Unterverzeichnis:
             try( ByteArrayInputStream is1 = new ByteArrayInputStream( meinText.getBytes( encoding ) ) ) {
                Assert.assertTrue( "storeFile", ffwrap.storeFile( is1, remoteResultFileName1 ) );
             }
             namesList = ffwrap.listFileNames();
             Assert.assertNotNull( "listNames", namesList );
             Assert.assertTrue( "listNames", namesList.contains( remoteResultFileName1 ) );
             Assert.assertTrue( "exists", ffwrap.existsFile( remoteResultFileName1 ) );
             Assert.assertEquals("getLength", meinText.getBytes( encoding ).length, ffwrap.getLength( remoteResultFileName1 ) );
    
             // Schreiben von Remote-Datei mit Unterverzeichnis:
             try( ByteArrayInputStream is2 = new ByteArrayInputStream( meinText.getBytes( encoding ) ) ) {
                Assert.assertTrue( "storeFile", ffwrap.storeFile( is2, remoteResultFile2 ) );
             }
             namesList = ffwrap.listDirNames();
             Assert.assertNotNull( "listNames", namesList );
             Assert.assertTrue( remoteResultFilePath2, namesList.contains( remoteResultFilePath2 ) );
             namesList = ffwrap.listFileNames( remoteResultFilePath2 );
             Assert.assertNotNull( "listNames", namesList );
             Assert.assertTrue( remoteResultFileName2, namesList.contains( remoteResultFileName2 ) );
             Assert.assertTrue( "exists", ffwrap.existsFile( remoteResultFile2 ) );
             Assert.assertEquals("getLength", meinText.getBytes( encoding ).length, ffwrap.getLength( remoteResultFile2 ) );
    
             // Alternative Schreibmethode mit OutputStream:
             ffwrap.deleteFile( remoteResultFileName1 );
             try( BufferedWriter out = new BufferedWriter( new OutputStreamWriter( ffwrap.storeFile( remoteResultFileName1 ), encoding ) ) ) {
                out.write( meinText );
             }
             Assert.assertTrue( "completePendingCommand", ffwrap.completePendingCommand() );
             // Mit append:
             try( BufferedWriter out = new BufferedWriter( new OutputStreamWriter( ffwrap.storeFile( remoteResultFileName1, true ), encoding ) ) ) {
                out.write( "\r\n" + meinText3 );
             }
             Assert.assertTrue( "completePendingCommand", ffwrap.completePendingCommand() );
    
             // Zip:
             try( InputStream  isForZip = new ByteArrayInputStream( meinText.getBytes( encoding ) );
                  OutputStream osZipped = ffwrap.storeFile( remoteResultFileName1 + ".zip" ) ) {
                FtpFileWrapper.zipStream( isForZip, osZipped, remoteResultFileName1 );
             }
             Assert.assertTrue( "completePendingCommand", ffwrap.completePendingCommand() );
    
             // Lesen von Datei ohne Unterverzeichnis:
             List<String> ss = ffwrap.retrieveTextLines( remoteResultFileName1, encoding, 100 );
             Assert.assertNotNull( "Text", ss );
             Assert.assertEquals( 3, ss.size() );
             Assert.assertEquals( meinText1, ss.get( 0 ) );
             Assert.assertEquals( meinText2, ss.get( 1 ) );
             Assert.assertEquals( meinText3, ss.get( 2 ) );
    
             // Lesen von Datei mit Unterverzeichnis:
             ss = ffwrap.retrieveTextLines( remoteResultFile2, encoding, 100 );
             Assert.assertNotNull( "Text", ss );
             Assert.assertEquals( 2, ss.size() );
             Assert.assertEquals( meinText1, ss.get( 0 ) );
             Assert.assertEquals( meinText2, ss.get( 1 ) );
    
             // changeWorkingDirectory():
             Assert.assertTrue( ffwrap.changeWorkingDirectory( remoteResultFilePath2 ) );
             namesList = ffwrap.listFileNames();
             Assert.assertTrue( "listNames", namesList.contains( remoteResultFileName2 ) );
             Assert.assertTrue( ffwrap.changeWorkingDirectory( "/" ) );
             namesList = ffwrap.listFileNames();
             Assert.assertTrue( "listNames", namesList.contains( remoteResultFileName1 ) );
    
             // moveRemoteFile() nach changeWorkingDirectory():
             Assert.assertTrue( ffwrap.changeWorkingDirectory( remoteResultFilePath2 ) );
             ffwrap.deleteFile( "/" + remoteResultFile3 );
             Assert.assertTrue( ffwrap.moveRemoteFile( remoteResultFileName2, "/" + remoteResultFile3 ) );
             Assert.assertTrue( ffwrap.changeWorkingDirectory( "/" ) );
             namesList = ffwrap.listDirNames();
             Assert.assertTrue( "moveRemoteFile(), Path", namesList.contains( remoteResultFilePath3 ) );
             namesList = ffwrap.listFileNames( remoteResultFilePath3 );
             Assert.assertTrue( "moveRemoteFile(), File", namesList.contains( remoteResultFileName3 ) );
             namesList = ffwrap.listFileNames( remoteResultFilePath2 );
             Assert.assertFalse( "moveRemoteFile(), Delete", namesList.contains( remoteResultFileName2 ) );
    
             // makeDirectory():
             ffwrap.makeDirectory( remotePath4a + "\\" + remotePath4b );
             namesList = ffwrap.listDirNames( remotePath4a );
             Assert.assertTrue( "makeDirectory()", namesList.contains( remotePath4b ) );
    
             // renameFile():
             try( ByteArrayInputStream is3 = new ByteArrayInputStream( "xy".getBytes() ) ) {
                Assert.assertTrue( "storeFile", ffwrap.storeFile( is3, remoteResultFile5 ) );
             }
             ffwrap.deleteFile( remoteResultFile6 );
             Assert.assertTrue( "renameFile", ffwrap.renameFile( remoteResultFile5, remoteResultFile6 ) );
             Assert.assertTrue( "existsFile", ffwrap.existsFile( remoteResultFile6 ) );
    
             // getFtpFsFileData():
             FtpFsFileData fd = ffwrap.getFtpFsFileData( "/" + remoteResultFile6 );
             Assert.assertEquals( "f.txt", fd.name );
             Assert.assertTrue(            fd.isFile );
             Assert.assertEquals(       2, fd.size );
             fd = ffwrap.getFtpFsFileData( "x.x" );
             Assert.assertEquals( null, fd.name );
    
             // Append: An Datei in Unterunterverzeichnis Text anhaengen:
             try( ByteArrayInputStream is4 = new ByteArrayInputStream( "z".getBytes() ) ) {
                Assert.assertTrue( "storeFile", ffwrap.storeFile( is4, remoteResultFile6, true ) );
             }
             ss = ffwrap.retrieveTextLines( remoteResultFile6, encoding, 100 );
             Assert.assertTrue( ss.size() == 1 );
             Assert.assertEquals( "xyz", ss.get( 0 ) );
    
             // listFtpFsFileDataRecursive():
             List<FtpFsFileData> fds;
             fds = ffwrap.listFtpFsFileDataRecursive( "/d1", "(/d1/d2|/d1/d2/.*)", null );
             Assert.assertEquals( "listFtpFsFileDataRecursive",                1, fds.size() );
             Assert.assertEquals( "listFtpFsFileDataRecursive", "/d1/d2/d3/f.txt", fds.get( 0 ).parentPath + "/" + fds.get( 0 ).name );
             Assert.assertEquals( "listFtpFsFileDataRecursive",                3, fds.get( 0 ).size );
             fds = ffwrap.listFtpFsFileDataRecursive( "d1", null, null );
             Assert.assertEquals( "listFtpFsFileDataRecursive",                1, fds.size() );
             Assert.assertEquals( "listFtpFsFileDataRecursive", "d1/d2/d3/f.txt", fds.get( 0 ).parentPath + "/" + fds.get( 0 ).name );
             Assert.assertEquals( "listFtpFsFileDataRecursive",                3, fds.get( 0 ).size );
             fds = ffwrap.listFtpFsFileDataRecursive( "d1", "(d1/d2|d1/d2/d3)", new HashSet<String>( Arrays.asList( new String[] { "xx" } ) ) );
             Assert.assertEquals( "listFtpFsFileDataRecursive",                1, fds.size() );
             Assert.assertEquals( "listFtpFsFileDataRecursive", "d1/d2/d3/f.txt", fds.get( 0 ).parentPath + "/" + fds.get( 0 ).name );
             Assert.assertEquals( "listFtpFsFileDataRecursive",                3, fds.get( 0 ).size );
             fds = ffwrap.listFtpFsFileDataRecursive( "d1", "xx", null );
             Assert.assertEquals( "listFtpFsFileDataRecursive", 0, fds.size() );
             fds = ffwrap.listFtpFsFileDataRecursive( "d1", null, new HashSet<String>( Arrays.asList( new String[] { "d2" } ) ) );
             Assert.assertEquals( "listFtpFsFileDataRecursive", 0, fds.size() );
    
             // Remove directory:
             Assert.assertTrue(  ffwrap.existsDir( "d1" ) );
             Assert.assertTrue(  ffwrap.removeDirectory( "d1" ) );
             Assert.assertFalse( ffwrap.existsDir( "d1" ) );
    
             // ObjectStreams:
             testFtpFileWrapperMitObjectStream( ffwrap );
    
             // Monitoring-Funktionen:
             long[] fileCountAndSizeSum = ffwrap.getFileCountAndSizeSum( null, null );
             FtpFsFileData oldestFileData = ffwrap.getOldestFileData( null, null, null );
             Assert.assertTrue( "fileCountAndSizeSum[0]: '" + fileCountAndSizeSum[0] + "' >= 3",   fileCountAndSizeSum[0] >= 3 );
             Assert.assertTrue( "fileCountAndSizeSum[1]: '" + fileCountAndSizeSum[1] + "' >= 152", fileCountAndSizeSum[1] >= 152 );
             Assert.assertTrue( "OldestFileData.timestamp", oldestFileData.timestamp.getTimeInMillis() > 1300000000000L );
             Assert.assertNotNull( "OldestFileData.name", oldestFileData.name );
    
          } finally {
             ffwrap.logout();
          }
       }
    
       /** Interne gemeinsame FtpFileWrapper-Testmethode fuer ObjectStreams sowohl fuer FTP- als auch FS-Betrieb */
       protected static void testFtpFileWrapperMitObjectStream( FtpFileWrapper ffwrap ) throws IOException
       {
          MyClass myObj1 = new MyClass();
          myObj1.s = "Blablupp";
          myObj1.i = 42;
    
          OutputStream os = ffwrap.storeFile( "MyObj.dat" );
          Assert.assertNotNull( "OutputStream null", os );
          try( ObjectOutputStream out = new ObjectOutputStream( os ) ) {
             out.writeObject( myObj1 );
             out.flush();
          } finally {
             ffwrap.completePendingCommand();
          }
    
          MyClass myObj2 = new MyClass();
          InputStream is = ffwrap.retrieveBinaryFile( "MyObj.dat" );
          Assert.assertNotNull( "InputStream null", is );
          try( ObjectInputStream in = new ObjectInputStream( is ) ) {
             myObj2 = (MyClass) in.readObject();
          } catch( ClassNotFoundException ex ) {
             Assert.fail( "ClassNotFoundException: " + ex.getMessage() );
          } finally {
             ffwrap.completePendingCommand();
          }
          Assert.assertNotNull( myObj2 );
          Assert.assertEquals( "Blablupp", myObj2.s );
          Assert.assertEquals(         42, myObj2.i );
       }
    }
    
    class MyClass implements Serializable
    {
       private static final long serialVersionUID = 1L;
       String s;
       int i;
    }
    
  8. Führen Sie den Test aus:

    cd \MeinWorkspace\FtpWrapper

    mvn test

    tree /F

  9. Nach der Ausführung des Tests erhalten Sie folgende Verzeichnisse und Dateien:

    [\MeinWorkspace\FtpWrapper]
     |- [src]
     |   |- [main]
     |   |   '- [java]
     |   |       '- [de]
     |   |           '- [meinefirma]
     |   |               '- [meinprojekt]
     |   |                   '- [ftp]
     |   |                       '- FtpFileWrapper.java
     |   '- [test]
     |       |- [java]
     |       |   '- [de]
     |       |       '- [meinefirma]
     |       |           '- [meinprojekt]
     |       |               '- [ftp]
     |       |                   |- FtpFileWrapperTest.java
     |       |                   '- FtpTestUtil.java
     |       '- [resources]
     |           |- FtpFileWrapper-FS.properties
     |           '- FtpFileWrapper-FTP.properties
     |- [target]
     |   |- [classes]
     |   |   '- ...
     |   |- [FileserverHome] . . . . . . . . . . . . [Home-Verzeichnis vom Fileserver]
     |   |   '- ...
     |   |- [FtpHome]  . . . . . . . . . . . . . . . [Home-Verzeichnis vom FTP-Server]
     |   |   '- ...
     |   |- [surefire-reports]
     |   |   '- ...
     |   '- [test-classes]
     |       '- ...
     '- pom.xml
    
  10. Die obigen Bemerkungen zu FTP mit FileZilla, cURL, Webbrowser und Windows-Explorer, sowie FTP-Server als Netzlaufwerk verbinden gelten auch für dieses Beispiel.

  11. Falls Sie einen OutputStream in einen InputStream wandeln müssen, hilft vielleicht folgendes Code-Snippet:

       PipedInputStream  in  = new PipedInputStream();
       PipedOUtputStream out = new PipedOutputStream( in );
       new Thread(
          new Runnable() {
             public void run() {
                // class1 muss eine beliebige putDataOnOutputStream()-Methode implementieren:
                class1.putDataOnOutputStream( out );
             }
          }
       ).start();
       // class2 muss eine beliebige processDataFromInputStream()-Methode implementieren:
       class2.processDataFromInputStream( in );
    

    Weitere Stream-Umwandlungen finden Sie in java-io.htm.



Stand-alone-FTP-Server

Der Apache FtpServer lässt sich leicht als Stand-alone-FTP-Server einsetzen, wie im Folgenden gezeigt wird.

Installation und Konfiguration des Apache FtpServers

  1. Downloaden Sie eine aktuelle Apache-FtpServer-Version, zum Beispiel ftpserver-1.0.6.zip, von http://mina.apache.org/ftpserver-project/downloads.html.

  2. Entpacken Sie das Archiv, zum Beispiel nach: D:\Tools.

  3. Der FtpServer benötigt zwei Konfigurationsdateien. Diese können in einem beliebigen Verzeichnis abgelegt werden, zum Beispiel im D:\Tools\apache-ftpserver-1.0.6\res\conf-Verzeichnis (wo sich Templates für die Konfigurationsdateien befinden) oder im D:\Tools\apache-ftpserver-1.0.6-Wurzelverzeichnis. Wir wählen letzteres, da wir auch noch Batchdateien hinzufügen wollen.

    Erzeugen Sie im FtpServer-Wurzelverzeichnis (z.B. D:\Tools\apache-ftpserver-1.0.6) folgende FTP-Server-Konfigurationsdatei: MeineFtpConfig.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <server xmlns="http://mina.apache.org/ftpserver/spring/v1"
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xsi:schemaLocation="http://mina.apache.org/ftpserver/spring/v1 http://mina.apache.org/ftpserver/ftpserver-1.0.xsd"
            id="myServer"
            max-threads="3000"
            max-logins="4000"
          	anon-enabled="true"
          	max-anon-logins="10"
          	max-login-failures="10"
          	login-failure-delay="1000">
      <listeners>
        <nio-listener name="default" port="4221" idle-timeout="300" />
      </listeners>
      <file-user-manager file="./MeineFtpUsers.properties" encrypt-passwords="salted" />
    </server>
    

    Prüfen Sie, ob die gewählte Portnummer (im Beispiel 4221) noch frei ist (wählen Sie andernfalls eine andere Portnummer):

    netstat -an | find "4221"

  4. In dieser MeineFtpConfig.xml steht ein Verweis auf die User-Manager-Datei, im Beispiel ./MeineFtpUsers.properties. Diese Datei erzeugen wir im Folgenden.

    Legen Sie diese MeineFtpUsers.properties-Datei vorerst als leere Textdatei im FtpServer-Wurzelverzeichnis an.

    Legen Sie im FtpServer-Wurzelverzeichnis folgende Batchdatei an:
    FtpServer-AddUser.bat

    title FTP-Server-AddUser.bat
    cls
    type MeineFtpConfig.xml
    type MeineFtpUsers.properties | sort
    @echo.
    @echo.
    @echo Als Home-Directory z.B. ./res/home oder C:\MeinFtpServerHome angeben
    java -cp common/classes;common/lib/ftpserver-core-1.0.6.jar;common/lib/* org.apache.ftpserver.main.AddUser MeineFtpConfig.xml
    type MeineFtpUsers.properties | sort
    

    Fügen Sie mit der Batchdatei benötigte Benutzer hinzu. Als Home-Directory können Sie beispielsweise ./res/home oder C:\MeinFtpServerHome angeben (beachten Sie dabei, dass die Eingabe von C:\MeinFtpServerHome zu dem Eintrag C\:\\MeinFtpServerHome führt).

    Üblich wäre etwa die Anlage eines anonymen Gastzugangs ohne Passwort und mit Nur-Lese-Rechten sowie eines weiteren Benutzers mit Schreibberechtigung. Hierfür beantworten Sie die gestellten Fragen folgendermaßen:
    "Anonymous": anonymous, <kein Passwort>, C:\MeinFtpServerHome, y, 300, n, 20, 2, 4800, 4800.
    "Write-User": WriteUserName, WriteUserPwd, C:\MeinFtpServerHome, y, 0, y, 0, 0, 0, 0.

    Sehen Sie sich das Ergebnis an (mit sortierter Zeilenreihenfolge):

    type MeineFtpUsers.properties | sort

  5. Legen Sie das angegebene FTP-Home-Directory (z.B. C:\MeinFtpServerHome) und darin eine Testdatei an:

    md C:\MeinFtpServerHome

    echo Hallo > C:\MeinFtpServerHome\Test.txt

  6. Wenn Sie auch die Loggingdatei des FtpServers im FtpServer-Wurzelverzeichnis haben wollen, ersetzen Sie den Inhalt der common\classes\log4j.properties-Datei zum Beispiel durch:

    log4j.rootLogger=WARN, R
    log4j.appender.R=org.apache.log4j.RollingFileAppender
    log4j.appender.R.File=./ftpd.log
    log4j.appender.R.MaxFileSize=10MB
    log4j.appender.R.MaxBackupIndex=10
    log4j.appender.R.layout=org.apache.log4j.PatternLayout
    log4j.appender.R.layout.ConversionPattern=[%5p] %d [%X{userName}] [%X{remoteIp}] %m%n
    

Manueller Start des FtpServers

  1. Legen Sie eine Batchdatei zum manuellen Starten des FTP-Servers an: FtpServer-Start-manuell.bat

    cd /D D:\Tools\apache-ftpserver-1.0.6
    start cmd /C bin\ftpd.bat MeineFtpConfig.xml
    start ftp://localhost:4221
    

    Starten Sie den FTP-Server mit dieser Batchdatei.

  2. Rufen Sie über ein FTP-Tool (cURL, Webbrowser, Windows-Explorer etc.) auf:

    ftp://localhost:4221

    ftp://WriteUserName:WriteUserPwd@localhost:4221

    (Ersetzen Sie die Parameter durch Ihre konfigurierten Werte.)

FtpServer als automatisch startender Windows-Dienst

  1. Sehen Sie sich die (sehr kurze) Kurzanleitung an: Installing FtpServer as a Windows service.

  2. Die Installation des Apache FtpServer als Windows-Dienst ist einfacher, wenn Sie hierfür ein 32-Bit-Java-JRE verwenden. Falls Sie ein 64-Bit-Windows haben, installieren Sie, falls noch nicht geschehen, zusätzlich zum 64-Bit-Java im C:\Program Files\Java-Verzeichnis auch ein 32-Bit-Java im C:\Program Files (x86)\Java-Verzeichnis. Damit bei zukünftigen Java-Updates nicht jedesmal der Java-Pfad angepasst werden muss, empfiehlt es sich, während der 32-Bit-Java-JRE-Installation den Zielverzeichnispfad von beispielsweise "C:\Program Files (x86)\Java\jre1.8.0_..." auf "C:\Program Files (x86)\Java\jre1.8" zu kürzen.

  3. Damit die Installation des FtpServers auch unter Java 8 funktioniert, muss im FtpServer-bin-Unterverzeichnis die Batchdatei service.bat erweitert werden. Suchen Sie darin die beiden Zeilen:

    set PR_JVM=%JAVA_HOME%\jre\bin\client\jvm.dll
    if exist "%PR_JVM%" goto foundJvm
    

    Fügen Sie dahinter hinzu:

    set PR_JVM=%JAVA_HOME%\bin\client\jvm.dll
    if exist "%PR_JVM%" goto foundJvm
    
  4. Legen Sie im FtpServer-Wurzelverzeichnis folgende Batchdatei an:
    FtpServer-Start-Dienst.bat

    @setlocal
    cls
    @echo Installation des Apache FtpServers als Windows-Dienst.
    @echo Eine Deinstallation kann erfolgen ueber:
    @echo     bin\service.bat remove
    @echo.
    netstat -an | find "4221"
    type MeineFtpConfig.xml
    type MeineFtpUsers.properties | sort
    @echo.
    set JAVA_HOME=%JAVA_HOME_32BIT%
    if "%JAVA_HOME%" == "" set JAVA_HOME=C:\Program Files (x86)\Java\jre1.8
    @echo.
    @echo JAVA_HOME=%JAVA_HOME%
    set PATH=%JAVA_HOME%\bin;%PATH%
    java -version
    if exist "%JAVA_HOME%\jre\bin\server\jvm.dll" goto _JvmDllOk
    if exist "%JAVA_HOME%\jre\bin\client\jvm.dll" goto _JvmDllOk
    if exist "%JAVA_HOME%\bin\client\jvm.dll" goto _JvmDllOk
    @echo FEHLER: jvm.dll nicht gefunden!
    goto _Fertig
    :_JvmDllOk
    @echo.
    del /Q res\log\jakarta_service_*.log
    @echo.
    call bin\service.bat install ftpd MeineFtpConfig.xml
    @echo on
    net start ftpd
    if not errorlevel 1 start ftp://localhost:4221
    type "res\log\jakarta_service_*.log"
    @echo.
    @echo Falls es Probleme gibt: Mit Admin-Rechten bin\ftpdw.exe starten und unter "Java / Java Virtual Maschine" den Pfad zu jvm.dll eintragen, z.B.:
    @echo     C:\Program Files (x86)\Java\jre1.8\bin\client\jvm.dll
    @echo.
    :_Fertig
    @endlocal
    
  5. Öffnen Sie ein Kommandozeilenfenster mit Admin-Rechten:
      entweder per:   Start | cmd eintippen | Strg+Shift+Enter betätigen,
      oder alternativ: Start | Alle Programme | Zubehör | mit rechter Maustaste auf "Eingabeaufforderung" | Klick auf "Als Administrator ausführen".
    In beiden Fällen öffnet sich ein Kommandozeilenfenster mit "Administrator" im Titel.

  6. Starten Sie im Admin-Kommandozeilenfenster den FTP-Server als Windows-Dienst (passen Sie den JAVA_HOME_32BIT-Pfad an):

    set JAVA_HOME_32BIT=C:\Program Files (x86)\Java\jre1.8

    cd /D D:\Tools\apache-ftpserver-1.0.6

    FtpServer-Start-Dienst.bat

  7. Normalerweise wird gewünscht, dass der FtpServer-Windows-Dienst beim Booten des PCs automatisch startet. Dies können Sie wahlweise entweder im "FtpServer Service administration GUI" über bin\ftpdw.exe oder alternativ wie bei allen Windows-Diensten üblich in der Windows-Dienste-Verwaltung einstellen. Letzteres liefert manchmal bessere Fehlermeldungen, weshalb dieser Weg beschrieben wird:
    Wählen Sie in Windows: "Start" | "Systemsteuerung" | ["System und Sicherheit"] | "Verwaltung" | "Dienste" | Doppelklick auf "Apache FtpServer ftpd" | Reiter "Allgemein" | "Starttyp: Automatisch".
    Normalerweise genügt das. Bei Schwierigkeiten können Sie zusätzlich versuchen:
    a) Im Reiter "Anmelden" wählen: "Dieses Konto", ...,
    b) Im Reiter "Wiederherstellung" bei "Erster Fehler" bis "Weitere Fehler" "Dienst neu starten" einstellen und bei "Dienst nach … Minuten neu starten" 1 Minute einstellen.

  8. Falls Sie in der Konsole beim "bin\service.bat install ..."-Kommando die Fehlermeldung erhalten:
    Failed installing 'ftpd' service 
    und die res\log\jakarta_service_...log meldet:
    [... service.c] [error] Zugriff verweigert
    [... prunsrv.c] [error] Unable to open the Service Manager

    Dann fehlen Admin-Rechte für die Installation. Öffnen Sie wie oben beschrieben ein neues Kommandozeilenfenster mit Admin-Rechten und führen Sie darin die genannten Kommandos aus.

  9. Falls Sie Probleme haben, rufen Sie zuerst bin\ftpdw.exe mit Admin-Rechten (per rechter Maustaste) auf, und überprüfen Sie alle Parameter, insbesondere unter dem Java-Reiter. Stellen Sie sicher, dass unter "Java / Java Virtual Maschine" ein gültiger Pfad zur jvm.dll eingetragen ist, beispielsweise: C:\Program Files (x86)\Java\jre1.8\bin\client\jvm.dll, und das diese Datei tatsächlich dort existiert.

    Überprüfen Sie außerdem in der Windows-Registry die Einträge unter:
    HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services\ftpd.

    Falls Sie in der Konsole beim "net start ftpd"-Kommando die Fehlermeldung erhalten:
    Apache FtpServer ftpd konnte nicht gestartet werden.
    Ein dienstspezifischer Fehler ist aufgetreten: 0.
    Sie erhalten weitere Hilfe, wenn Sie NET HELPMSG 3547 eingeben.
    Dann sehen Sie sich den Inhalt der res\log\jakarta_service_...log an.

    Falls die res\log\jakarta_service_...log meldet:
    [... javajni.c] [error] Das angegebene Modul wurde nicht gefunden.
    [... prunsrv.c] [error] Failed creating java
    Dann stimmt etwas mit dem Pfad zur jvm.dll nicht. Untersuchen Sie dies wie oben beschrieben.

    Falls die res\log\jakarta_service_...log meldet:
    [... javajni.c] [error] %1 ist keine zulässige Win32-Anwendung.
    [... prunsrv.c] [error] Failed creating java
    [... prunsrv.c] [error] ServiceStart returned 1
    Dann haben Sie wahrscheinlich ein 64-Bit-Windows und entweder kein 32-Bit-Java installiert (siehe oben) oder nicht die JAVA_HOME_32BIT-Umgebungsvariable korrekt gesetzt (siehe oben).

  10. Falls Sie beim Start des FTP-Servers als Windows-Dienst diese Fehlermeldung erhalten:

    Der Dienst ... konnte nicht gestartet werden. Fehler 1067: Der Prozess wurde unerwartet beendet.

    und in der res\log\jakarta_service_...log steht:

    [info] Error occurred during initialization of VM, java/lang/NoClassDefFoundError : java/lang/Object

    Dann überprüfen Sie Ihre Java-JRE-Installation (passen Sie den Pfad an):

    cd /D "C:\Program Files (x86)\Java\jre1.8\bin"

    java –version

    Wenn Sie z.B. erhalten:

    Error occurred during initialization of VM

    java/lang/NoClassDefFoundError: java/lang/Object

    Dann war vielleicht ein Update nicht erfolgreich. Deinstallieren Sie das Java und installieren es neu.

  11. Eventuell finden Sie auch brauchbare Fehlermeldungen in der Windows-Ereignisanzeige (unter Start | Systemsteuerung | [System und Sicherheit] | Verwaltung | Computerverwaltung | System | Ereignisanzeige).

  12. So können Sie den FtpServer-Windows-Dienst deinstallieren:

    cd /D D:\Tools\apache-ftpserver-1.0.6

    bin\service.bat remove

Test des FtpServers mit dem FtpFileWrapper-Test

  1. Um die oben gezeigte FtpFileWrapper-Klasse mit einem stand-alone FTP-Server zu testen, können Sie im \MeinWorkspace\FtpWrapper\src\test\java\de\meinefirma\meinprojekt\ftp-Verzeichnis folgenden Test erzeugen: FtpStandAloneIT.java

    package de.meinefirma.meinprojekt.ftp;
    
    import java.io.IOException;
    
    public class FtpStandAloneIT
    {
       public static void main( String[] args ) throws IOException
       {
          String uri = ( args.length > 0 ) ? args[0] : "ftp://WriteUserName:WriteUserPwd@localhost:4221";
          System.out.println( "URI: " + uri );
          FtpFileWrapper ffwrap = FtpFileWrapper.createFtpFileWrapperFromUri( uri );
          FtpFileWrapperTest.testFtpFileWrapper( ffwrap, true );
          ffwrap.logout();
          System.out.println( "ok" );
       }
    }
    

    Nach der Testausführung finden Sie unter ftp://WriteUserName:WriteUserPwd@localhost:4221 die vom Test angelegten Verzeichnisse und Dateien.





Weitere Themen: andere TechDocs | SFTP | FTP | FTPClient | FtpServer
© 2011 Torsten Horn, Aachen