web-dev-qa-db-ja.com

Apache commons-net FTPClientで生のバイナリを転送しますか?

更新:解決済み

私はFTPClient.setFileType()を呼び出していましたbeforeログインして、FTPサーバーがデフォルトモード(ASCII)を使用するようにしましたどんなに私はそれを設定しました。一方、クライアントは、ファイルタイプが適切に設定されているかのように動作していました。 BINARYモードが希望どおりに機能するようになり、すべてのケースでバイト単位でファイルが転送されます。私がしなければならなかったのは、wiresharkでトラフィックをスニッフィングし、netcatを使用してFTPコマンドを模倣して、何が起こっているのかを確認することだけでした。なぜ二日前にそんなことを考えなかったんだ!みんな、助けてくれてありがとう!

Utf-16エンコードされたxmlファイルがあり、Apacheのcommons-net-2.0 JavaライブラリのFTPClientを使用してFTPサイトからダウンロードしています。2つの転送モード(ASCII_FILE_TYPEBINARY_FILE_TYPE)をサポートしています。 、違いはASCIIが行区切り文字を適切なローカル行区切り文字('\r\n'または単に'\n'-16進数、0x0d0aまたは0x0a)で置き換えることです。私の問題はこれです:テストファイルがあります。utf- 16エンコードされ、以下が含まれます。

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

これが16進数です。
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.>..

このファイルにASCIIモードを使用すると、バイト単位で正しく転送されます。結果は同じmd5sumになります。すごい。 BINARY転送モードを使用すると、InputStreamからOutputStreamにバイトをシャッフルする以外は何も行われません。その結果、改行(0x0a)が変換されます改行+改行のペア(0x0d0a)。バイナリ転送後の16進数は次のとおりです。

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 0d0at.a.>...

改行文字を変換するだけでなく(変換すべきではありません)、utf-16エンコーディングを尊重しません(変換する必要があることを知っていると私が期待するのではなく、単なるFTPパイプです)。結果は、バイトを再調整するためのさらなる処理を行わない限り、読み取ることができません。私はASCIIモードを使用するだけですが、私のアプリケーションも同じパイプを介してrealバイナリデータ(mp3ファイルとjpeg画像)を移動します。これらのバイナリファイルでBINARY転送モードを使用すると、コンテンツにランダムな0x0dsがランダムに挿入されます。バイナリデータには正当な0x0d0aシーケンスが含まれていることが多いため、安全に削除することはできません。これらのファイルでASCIIモードを使用すると、「賢い」FTPClientがこれらの0x0d0asを0x0aに変換し、ファイルをどのようにしても一貫性が失われます。

私の質問は(ある)と思います:Javaのための良いFTPライブラリを誰かが知っていますか?それは、そこから邪魔なバイトをそこからここに移動するだけですか、それともハックする必要がありますか? Apache commons-net-2.0を起動して、この単純なアプリケーションのためだけに自分のFTPクライアントコードを維持しますか?この奇妙な動作を他の誰かが扱っていませんか?何か提案があれば幸いです。

Commons-netのソースコードをチェックアウトしましたが、BINARYモードが使用されているときの奇妙な動作の原因ではないようです。ただし、InputStreamモードで読み取るBINARYは、ソケットInputStreamにラップされたJava.io.BufferedInptuStreamにすぎません。これらの下位レベルはJavaストリームが奇妙なバイト操作を行うことはありますか?実行するとショックを受けるでしょうが、他に何が起こっているのかわかりません。

編集1:

これは、ファイルをダウンロードするために私がしていることを模倣した最小限のコードです。コンパイルするには、次のようにします

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

実行するには、ファイルをダウンロードするディレクトリ/ tmp/asciiおよび/ tmp/binaryが必要です。また、ファイルが置かれたFTPサイトがセットアップされている必要があります。コードは、適切なFTPホスト、ユーザー名、パスワードで構成する必要もあります。このファイルをテスト用のftpサイトのtest /フォルダーの下に置き、ファイルtest.xmlを呼び出しました。テストファイルは少なくとも1行以上必要で、utf-16でエンコードする必要があります(これは必要ないかもしれませんが、私の正確な状況を再現するのに役立ちます)。新しいファイルを開いて上記のxmlテキストを入力した後、vimの:set fileencoding=utf-16コマンドを使用しました。最後に、実行するには、単に

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

コード:

(注:このコードは、「EDIT 2」の下でリンクされているカスタムFTPClientオブジェクトを使用するように変更されています)

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;
    }

}

編集2:

さて、CheckedXputStreamのアドバイスに従いました。これが私の結果です。 FTPClientというApacheのMyFTPClientのコピーを作成し、SocketInputStreamBufferedInputStreamの両方をCRC32チェックサムを使用してCheckedInputStreamにラップしました。さらに、出力をFileOutputStreamCRC32チェックサムで格納するために、FTPClientに与えたCheckOutputStreamをラップしました。 MyFTPClientのコードが投稿されました ここ と、このバージョンのFTPClientを使用するように上記のテストコードを変更しました(変更されたコードにGist URLを投稿しようとしましたが、投稿するには10のレピュテーションポイントが必要です複数のURL!)、test.xmltest.mp3、および結果は次のとおりです。

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

これは、対応するファイルのmd5sumがあるため、基本的にはまったく意味がありません。

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

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

私は途方に暮れています。私swear私はこのプロセスのどの時点でもファイル名/パスを変更していません。また、すべてのステップをトリプルチェックしました。それは単純なものでなければなりませんが、どこを見ればよいのかわかりません。実用性を考慮して、FTP転送を行うためにシェルを呼び出すことから始めますが、地獄が何が起こっているのか理解するまで、これを追求するつもりです。私はこのスレッドを私の調査結果で更新します。そして、誰もが持つかもしれない貢献に感謝します。うまくいけば、これはいつか誰かに役立つでしょう!

27
Chris Suter

FTPサーバーにログインした後

ftp.setFileType(FTP.BINARY_FILE_TYPE);

以下の行はそれを解決しません:

//ftp.setFileTransferMode(org.Apache.commons.net.ftp.FTP.BINARY_FILE_TYPE);
32
Sven

アプリケーションコードでASCIIの選択とBINARYモードが反転しているように思えます。 ASCIIは変更されずに送信されます。行末文字変換を実行するBINARYは、FTPの動作方法の正反対です。

それが問題でない場合は、質問を編集して、コードの関連部分を追加してください。

[〜#〜]編集[〜#〜]

他にいくつか考えられる(ただしIMOの可能性は低い)説明:

  • FTPサーバーが壊れているか、正しく設定されていません。 (Java以外のコマンドラインFTPユーティリティを使用してASCII/BINARYモードでファイルを正常にダウンロードできますか?)
  • 壊れているか、正しく設定されていないプロキシを介してFTPサーバーと通信しています。
  • どういうわけか、Apache FTPクライアントJARファイルの危険な(ハッキングされた)コピーを入手できました。 (そう、そう、そうそう...)
4
Stephen C

Apache retrieveFile(...)が、特定の制限を超えるファイルサイズで機能しないことがありました。これを克服するには、代わりにretrieveFileStream()を使用します。ダウンロードする前に、正しいファイルタイプを設定し、モードをパッシブモードに設定しました

したがって、コードは次のようになります

    ....
    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();
    ....
3
Vivek Kumar