2010-06-30 27 views
24

UPDATE: SolvedPrzenieść surowy plik binarny za pomocą apache commons-net FTPClient?

byłem nazywając FTPClient.setFileType()przed ja zalogowany, powodując serwer FTP, aby użyć trybu domyślnego (ASCII) bez względu co ustawić go. Z drugiej strony, klient zachowywał się tak, jakby typ pliku był prawidłowo ustawiony. Tryb BINARY działa teraz dokładnie tak, jak jest to wymagane, w każdym przypadku przenosząc bajt po bajcie. Jedyne, co musiałem zrobić, to trochę ruchu w węźle wireshark, a następnie naśladowanie poleceń FTP za pomocą netcata, żeby zobaczyć, co się dzieje. Dlaczego nie pomyślałem o tym dwa dni temu !? Dzięki, wszyscy za pomoc!

Mam plik xml, zakodowany w utf-16, który pobieram z witryny FTP przy użyciu FTPClienta apache's commons-net-2.0 java library. Oferuje wsparcie dla dwóch trybów transferu: ASCII_FILE_TYPE i BINARY_FILE_TYPE, z tą różnicą, że ASCII zastąpi separatory linii odpowiednim lokalnym separatorem linii ('\r\n' lub po prostu '\n' - w języku heksadecymalnym, 0x0d0a lub po prostu 0x0a).Mój problem jest następujący: Mam plik testowy, UTF-16 kodowany, który zawiera następujące elementy:

<?xml version='1.0' encoding='utf-16'?>
<data>
        <blah>blah</blah>
</data>

Oto hex:
0000000: 003c 003f 0078 006d 006c 0020 0076 0065 .<.?.x.m.l. .v.e
0000010: 0072 0073 0069 006f 006e 003d 0027 0031 .r.s.i.o.n.=.'.1
0000020: 002e 0030 0027 0020 0065 006e 0063 006f ...0.'. .e.n.c.o
0000030: 0064 0069 006e 0067 003d 0027 0075 0074 .d.i.n.g.=.'.u.t
0000040: 0066 002d 0031 0036 0027 003f 003e 000a .f.-.1.6.'.?.>..
0000050: 003c 0064 0061 0074 0061 003e 000a 0009 .<.d.a.t.a.>....
0000060: 003c 0062 006c 0061 0068 003e 0062 006c .<.b.l.a.h.>.b.l
0000070: 0061 0068 003c 002f 0062 006c 0061 0068 .a.h.<./.b.l.a.h
0000080: 003e 000a 003c 002f 0064 0061 0074 0061 .>...<./.d.a.t.a
0000090: 003e 000a                                                                                                                       .>..

Kiedy korzystać z trybu ASCII dla tego pliku przenosi poprawnie, bajt dla bajcie; wynik ma taką samą sumę md5. Wspaniały. Kiedy używam trybu transferu BINARY, który nie powinien wykonywać niczego poza losowaniem bajtów z InputStream do OutputStream, wynikiem jest, że znaki nowej linii (0x0a) są konwertowane na pary powrotu karetki i nowego wiersza (0x0d0a).Oto hex po transferze binarnym:

0000000: 003c 003f 0078 006d 006c 0020 0076 0065 .<.?.x.m.l. .v.e
0000010: 0072 0073 0069 006f 006e 003d 0027 0031 .r.s.i.o.n.=.'.1
0000020: 002e 0030 0027 0020 0065 006e 0063 006f ...0.'. .e.n.c.o
0000030: 0064 0069 006e 0067 003d 0027 0075 0074 .d.i.n.g.=.'.u.t
0000040: 0066 002d 0031 0036 0027 003f 003e 000d .f.-.1.6.'.?.>..
0000050: 0a00 3c00 6400 6100 7400 6100 3e00 0d0a ..<.d.a.t.a.>...
0000060: 0009 003c 0062 006c 0061 0068 003e 0062 ...<.b.l.a.h.>.b
0000070: 006c 0061 0068 003c 002f 0062 006c 0061 .l.a.h.<./.b.l.a
0000080: 0068 003e 000d 0a00 3c00 2f00 6400 6100 .h.>....<./.d.a.
0000090: 7400 6100 3e00 0d0a                                                                                 t.a.>...

Nie tylko przekonwertować znaki nowej linii (które nie powinny), ale nie przestrzega kodowanie UTF-16 (nie, że będę oczekiwać, że wie, że powinien , to tylko głupia rura FTP). Rezultat jest nieczytelny bez dalszego przetwarzania w celu wyrównania bajtów. Po prostu użyłbym trybu ASCII, ale moja aplikacja będzie również przenosić prawdziwe dane binarne (pliki mp3 i obrazy JPEG) w tej samej rurze. Użycie trybu transferu BINARY na tych plikach binarnych powoduje również, że są one losowo wstrzykiwane do ich zawartości, które nie mogą być bezpiecznie usunięte, ponieważ dane binarne często zawierają prawidłowe sekwencje . Jeśli używam trybu ASCII na tych plikach, "sprytny" klient FTP konwertuje te 0x0d0a s na 0x0a, pozostawiając plik niespójny bez względu na to, co zrobię.

Domyślam się, że moje pytanie (s) jest (są): czy ktoś wie o dobrych bibliotekach FTP dla java, które przenoszą te cholerne bajty stamtąd do tego miejsca, czy też będę musiał zhakować apache commons-net -2.0 i utrzymywać mój własny kod klienta FTP właśnie dla tej prostej aplikacji? Czy ktoś zajmował się tym dziwacznym zachowaniem? Wszelkie sugestie będą mile widziane.

Sprawdziłem kod źródłowy commons-net i nie wygląda na to, że odpowiada za dziwne zachowanie, gdy używany jest tryb BINARY. Ale InputStream to odczyt z trybu BINARY to tylko java.io.BufferedInptuStream owinięty wokół gniazda InputStream. Czy te strumienie java niższego poziomu kiedykolwiek robią jakąś dziwną manipulację bajtów? Byłbym zszokowany, gdyby tak zrobili, ale nie widzę, co jeszcze mogłoby się tu wydarzyć.

EDIT 1:

Oto minimalny fragment kodu, który naśladuje to, co robię, aby pobrać plik.Aby skompilować, zrób

javac -classpath /path/to/commons-net-2.0.jar Main.java 

Aby uruchomić, trzeba katalogi/tmp/ASCII i/tmp/binarne dla pliku do pobrania na, jak również ftp utworzonej z plikiem na nim siedzi . Kod będzie musiał również zostać skonfigurowany za pomocą odpowiedniego hosta ftp, nazwy użytkownika i hasła. Umieściłem plik na mojej testowej stronie ftp w folderze test/i nazwałam plik test.xml. Plik testowy powinien mieć co najmniej więcej niż jeden wiersz i być zakodowany za pomocą UTF-16 (może to nie być konieczne, ale pomoże odtworzyć moją dokładną sytuację). Po otwarciu nowego pliku użyłem polecenia vim o numerze :set fileencoding=utf-16 i wprowadziłem powyższy tekst xml. Na koniec, aby uruchomić, po prostu zrób

java -cp .:/path/to/commons-net-2.0.jar Main 

Kod:

(UWAGA: Ten kod zmodyfikowane w celu zastosowania niestandardowego FTPClient obiektu, związane poniżej: "EDIT 2")

import java.io.*; 
import java.util.zip.CheckedInputStream; 
import java.util.zip.CheckedOutputStream; 
import java.util.zip.CRC32; 
import org.apache.commons.net.ftp.*; 

public class Main implements java.io.Serializable 
{ 
    public static void main(String[] args) throws Exception 
    { 
     Main main = new Main(); 
     main.doTest(); 
    } 

    private void doTest() throws Exception 
    { 
     String host = "ftp.host.com"; 
     String user = "user"; 
     String pass = "pass"; 

     String asciiDest = "/tmp/ascii"; 
     String binaryDest = "/tmp/binary"; 

     String remotePath = "test/"; 
     String remoteFilename = "test.xml"; 

     System.out.println("TEST.XML ASCII"); 
     MyFTPClient client = createFTPClient(host, user, pass, org.apache.commons.net.ftp.FTP.ASCII_FILE_TYPE); 
     File path = new File("/tmp/ascii"); 
     downloadFTPFileToPath(client, "test/", "test.xml", path); 
     System.out.println(""); 

     System.out.println("TEST.XML BINARY"); 
     client = createFTPClient(host, user, pass, org.apache.commons.net.ftp.FTP.BINARY_FILE_TYPE); 
     path = new File("/tmp/binary"); 
     downloadFTPFileToPath(client, "test/", "test.xml", path); 
     System.out.println(""); 

     System.out.println("TEST.MP3 ASCII"); 
     client = createFTPClient(host, user, pass, org.apache.commons.net.ftp.FTP.ASCII_FILE_TYPE); 
     path = new File("/tmp/ascii"); 
     downloadFTPFileToPath(client, "test/", "test.mp3", path); 
     System.out.println(""); 

     System.out.println("TEST.MP3 BINARY"); 
     client = createFTPClient(host, user, pass, org.apache.commons.net.ftp.FTP.BINARY_FILE_TYPE); 
     path = new File("/tmp/binary"); 
     downloadFTPFileToPath(client, "test/", "test.mp3", path); 
    } 

    public static File downloadFTPFileToPath(MyFTPClient ftp, String remoteFileLocation, String remoteFileName, File path) 
     throws Exception 
    { 
     // path to remote resource 
     String remoteFilePath = remoteFileLocation + "/" + remoteFileName; 

     // create local result file object 
     File resultFile = new File(path, remoteFileName); 

     // local file output stream 
     CheckedOutputStream fout = new CheckedOutputStream(new FileOutputStream(resultFile), new CRC32()); 

     // try to read data from remote server 
     if (ftp.retrieveFile(remoteFilePath, fout)) { 
      System.out.println("FileOut: " + fout.getChecksum().getValue()); 
      return resultFile; 
     } else { 
      throw new Exception("Failed to download file completely: " + remoteFilePath); 
     } 
    } 

    public static MyFTPClient createFTPClient(String url, String user, String pass, int type) 
     throws Exception 
    { 
     MyFTPClient ftp = new MyFTPClient(); 
     ftp.connect(url); 
     if (!ftp.setFileType(type)) { 
      throw new Exception("Failed to set ftpClient object to BINARY_FILE_TYPE"); 
     } 

     // check for successful connection 
     int reply = ftp.getReplyCode(); 
     if (!FTPReply.isPositiveCompletion(reply)) { 
      ftp.disconnect(); 
      throw new Exception("Failed to connect properly to FTP"); 
     } 

     // attempt login 
     if (!ftp.login(user, pass)) { 
      String msg = "Failed to login to FTP"; 
      ftp.disconnect(); 
      throw new Exception(msg); 
     } 

     // success! return connected MyFTPClient. 
     return ftp; 
    } 

} 

EDIT 2:

OK Poprosiłem o poradę CheckedXputStream i oto moje wyniki. Wykonałem kopię apache'a o nazwie FTPClient o nazwie MyFTPClient i zapakowałem zarówno SocketInputStream, jak i BufferedInputStream w postaci sum kontrolnych CheckedInputStream. Ponadto zapakowałem FileOutputStream, który podaję FTPClient, aby zapisać dane wyjściowe w postaci z sumą kontrolną CRC32. Kod dla MyFTPClient został opublikowany here i zmodyfikowałem powyższy kod testowy, aby korzystać z tej wersji FTPClient (próbowałem opublikować adres URL do zmodyfikowanego kodu, ale potrzebuję 10 punktów reputacji, aby opublikować więcej niż jeden URL!), test.xml i test.mp3 a wyniki były zatem:

14:00:08,644 DEBUG [main,TestMain] TEST.XML ASCII 
14:00:08,919 DEBUG [main,MyFTPClient] Socket CRC32: 2739864033 
14:00:08,919 DEBUG [main,MyFTPClient] Buffer CRC32: 2739864033 
14:00:08,954 DEBUG [main,FTPUtils] FileOut CRC32: 866869773 

14:00:08,955 DEBUG [main,TestMain] TEST.XML BINARY 
14:00:09,270 DEBUG [main,MyFTPClient] Socket CRC32: 2739864033 
14:00:09,270 DEBUG [main,MyFTPClient] Buffer CRC32: 2739864033 
14:00:09,310 DEBUG [main,FTPUtils] FileOut CRC32: 2739864033 

14:00:09,310 DEBUG [main,TestMain] TEST.MP3 ASCII 
14:00:10,635 DEBUG [main,MyFTPClient] Socket CRC32: 60615183 
14:00:10,635 DEBUG [main,MyFTPClient] Buffer CRC32: 60615183 
14:00:10,636 DEBUG [main,FTPUtils] FileOut CRC32: 2352009735 

14:00:10,636 DEBUG [main,TestMain] TEST.MP3 BINARY 
14:00:11,482 DEBUG [main,MyFTPClient] Socket CRC32: 60615183 
14:00:11,482 DEBUG [main,MyFTPClient] Buffer CRC32: 60615183 
14:00:11,483 DEBUG [main,FTPUtils] FileOut CRC32: 60615183 

To sprawia, że ​​w zasadzie zerowy sens w ogóle, ponieważ tutaj są sumy md5 plików corresponsing:

bf89673ee7ca819961442062eaaf9c3f ascii/test.mp3 
7bd0e8514f1b9ce5ebab91b8daa52c4b binary/test.mp3 
ee172af5ed0204cf9546d176ae00a509 original/test.mp3 

104e14b661f3e5dbde494a54334a6dd0 ascii/test.xml 
36f482a709130b01d5cddab20a28a8e8 binary/test.xml 
104e14b661f3e5dbde494a54334a6dd0 original/test.xml 

Jestem na straty. I przysięga Nie permutowałem nazw plików/ścieżek w żadnym punkcie tego procesu, a trzy razy sprawdzałem każdy krok. To musi być coś prostego, ale nie mam zielonego pojęcia, gdzie szukać dalej. W interesie praktyczności zamierzam kontynuować, wołając do powłoki, aby wykonać moje transfery FTP, ale zamierzam to kontynuować, dopóki nie zrozumiem, co do diabła się dzieje. Zaktualizuję ten wątek o moje odkrycia i będę nadal doceniać wszelkie składki, które ktoś może mieć. Mam nadzieję, że przyda się to komuś w pewnym momencie!

+2

Wow, to dziwne. Sprawdziłem kod źródłowy dla 'BufferedInputStream' oraz' SocketInputStream' (przynajmniej część Java) i nie widzę niczego, co mogłoby zmienić takie bajty. Proponuję zrobić kopię 'FTPClient' i zmienić hierarchię strumienia wejściowego na' CheckedInputStream (BufferedInputStream (CheckedInputStream (SocketInputStream())))) i użyć sum kontrolnych, aby sprawdzić, czy możesz zidentyfikować, gdzie zmieniają się bajty. To byłaby przydatna informacja w pytaniu. (Jeszcze lepiej, umieść swój kod testowy online i dołącz do niego link). –

+1

Również +1 za dobrze napisane pytanie ;-) –

+0

Spróbuję; Dziękuję Ci. Nigdy nie słyszałem o CheckedInputStream. Bardzo fajny!! – cgs1019

Odpowiedz

4

To brzmi dla mnie tak, jakby twój kod aplikacji mógł wybrać tryb ASCII i BINARY odwrócony. ASCII przechodzi przez niezmienione, BINARY wykonujące tłumaczenia końca linii jest dokładnie przeciwne, jak FTP ma działać.

Jeśli to nie jest problem, edytuj swoje pytanie, aby dodać odpowiednią część kodu.

EDIT

Kilka innych możliwych (ale IMO mało prawdopodobne) wyjaśnienia:

  • Serwer FTP jest uszkodzony/źle skonfigurowany. (Czy z powodzeniem można pobrać plik w trybie ASCII/BINARY przy użyciu narzędzia FTP działającego w wierszu polecenia innym niż Java?)
  • Mówisz do serwera FTP przez serwer proxy, który jest uszkodzony lub źle skonfigurowany.
  • Udało ci się jakoś zdobyć podejrzaną (zhakowaną) kopię pliku JAR klienta Apache FTP. (Tak, tak, bardzo mało prawdopodobne ...)
+0

Wyglądałoby to tak, ale uruchomiłem kod co najmniej 5 razy i usunąłem jak najwięcej zmiennych. Zmodyfikowałem mój post, dołączając kod, który zweryfikowałem, który odtwarza problem. Niestety nie mogę zaoferować strony ftp, z której można pobrać plik, więc mam nadzieję, że masz dostęp do jednego (ja właśnie testuję na localhost). Dziękuję za odpowiedź. Doceniam to, jeśli będziesz miał jakieś myśli do przekazania! – cgs1019

+0

Rozważyłem pierwszy przypadek, który wymieniłeś jako najbardziej prawdopodobne wyjaśnienie, jeśli kod jest poprawny. Jest to całkiem domyślna instalacja proftp na Ubuntu. Po prostu próbowałem pobierać za pomocą standardowego klienta wiersza poleceń ftp, a plik xml przychodzi dobrze (można się tego spodziewać, ponieważ klient prawdopodobnie używa trybu ASCII, który poprawnie przesłał xml za pomocą FTPClient). Przesyła również plik mp3 poprawnie (ten sam md5sum), więc nie wygląda na serwer, chyba że FTPClient łączy się z nim z innymi ustawieniami niż klient linii cmd (możliwość). – cgs1019

+1

Również, bym cię upomniał o twoją pomoc, ale nie mam jeszcze 15 punktów rep! :) – cgs1019

25

Po zalogowaniu się do serwera FTP

ftp.setFileType(FTP.BINARY_FILE_TYPE); 

Linia poniżej nie rozwiązuje go:

//ftp.setFileTransferMode(org.apache.commons.net.ftp.FTP.BINARY_FILE_TYPE); 
+0

Dzięki, to mi się udało. Dziwne, że tryb tekstowy byłby domyślny. – Davor

2

Okazało się, że Apache Funkcja retrieveFile (...) czasami nie działała, gdy rozmiary plików przekraczają określony limit. Aby temu zaradzić, użyłbym metody retrieveFileStream(). Przed ściągnąć mam ustawić prawidłową FileType i ustawić tryb PassiveMode

Więc kod będzie wyglądał

.... 
    ftpClientConnection.setFileType(FTP.BINARY_FILE_TYPE); 
    ftpClientConnection.enterLocalPassiveMode(); 
    ftpClientConnection.setAutodetectUTF8(true); 

    //Create an InputStream to the File Data and use FileOutputStream to write it 
    InputStream inputStream = ftpClientConnection.retrieveFileStream(ftpFile.getName()); 
    FileOutputStream fileOutputStream = new FileOutputStream(directoryName + "/" + ftpFile.getName()); 
    //Using org.apache.commons.io.IOUtils 
    IOUtils.copy(inputStream, fileOutputStream); 
    fileOutputStream.flush(); 
    IOUtils.closeQuietly(fileOutputStream); 
    IOUtils.closeQuietly(inputStream); 
    boolean commandOK = ftpClientConnection.completePendingCommand(); 
    ....