Перенести необработанный двоичный файл с помощью apache commons-net FTPClient?

ОБНОВЛЕНИЕ: решено

Я звонил FTPClient.setFileType() до того, как я вошел в систему, в результате чего FTP-сервер использовал режим по умолчанию (ASCII) независимо от того, какой я его установил. С другой стороны, клиент вёл себя так, как если бы тип файла был установлен правильно. Режим BINARY теперь работает точно так, как нужно, во всех случаях побайтно передавая файл. Все, что мне нужно было сделать, это немного проанализировать трафик в WireShark, а затем имитировать команды FTP с помощью netcat, чтобы увидеть, что происходит. Почему я не подумал об этом два дня назад !? Спасибо всем за вашу помощь!

У меня есть XML-файл в кодировке utf-16, который я загружаю с FTP-сайта, используя FTPClient библиотеки apache commons-net-2.0 java. Он предлагает поддержку двух режимов передачи: ASCII_FILE_TYPE и BINARY_FILE_TYPE, разница в том, что ASCII заменяет разделители строк на соответствующий локальный разделитель строк ('\r\n' или просто '\n' - в шестнадцатеричном формате, 0x0d0a или просто 0x0a). Моя проблема заключается в следующем: у меня есть тестовый файл в кодировке utf-16, который содержит следующее:

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

Вот шестнадцатеричное значение:
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 < br /> 0000090: 003e 000a .>..

Когда я использую режим ASCII для этого файла, он передает правильно, побайтно; результат имеет тот же md5sum. Здорово. Когда я использую режим передачи BINARY, который не должен делать ничего, кроме перетасовки байтов из InputStream в OutputStream, в результате символы новой строки (0x0a) преобразуются в пары "возврат каретки + новая строка" (0x0d0a). Вот шестнадцатеричный код после двоичной передачи:

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.>...

Он не только преобразует символы новой строки (чего не следует), но и не соблюдает кодировку utf-16 (не то, чтобы я ожидал, что он знает, что должен, это просто тупой FTP-канал). Результат нечитаем без дальнейшей обработки для повторного выравнивания байтов. Я бы просто использовал режим ASCII, но мое приложение также будет перемещать реальные двоичные данные (файлы mp3 и изображения jpeg) по тому же каналу. Использование BINARY режима передачи для этих двоичных файлов также приводит к тому, что в их содержимое вводятся случайные 0x0d, которые нельзя безопасно удалить, поскольку двоичные данные часто содержат допустимые 0x0d0a последовательности. Если я использую ASCII режим для этих файлов, то "умный" FTPClient преобразует эти 0x0d0a в 0x0a, оставляя файл несовместимым, что бы я ни делал.

Я предполагаю, что мой вопрос (вопросы): кто-нибудь знает какие-либо хорошие FTP-библиотеки для java, которые просто перемещают проклятые байты оттуда сюда, или мне придется взломать apache commons-net-2.0 и поддерживать мой собственный код FTP-клиента только для этого простого приложения? Кто-нибудь еще имел дело с таким странным поведением? Мы ценим любые предложения.

Я проверил исходный код commons-net, и не похоже, что он отвечает за странное поведение при использовании режима BINARY. Но InputStream, из которого он читает в режиме BINARY, всего лишь java.io.BufferedInptuStream, обернутый вокруг сокета InputStream. Выполняют ли эти потоки Java более низкого уровня какие-либо странные манипуляции с байтами? Я был бы шокирован, если бы они это сделали, но я не понимаю, что еще здесь может происходить.

РЕДАКТИРОВАТЬ 1:

Вот минимальный фрагмент кода, который имитирует то, что я делаю для загрузки файла. Для компиляции просто выполните

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

Для запуска вам потребуются каталоги / tmp / ascii и / tmp / binary для загрузки файла, а также настроенный ftp-сайт с файлом, находящимся на нем. В коде также необходимо указать соответствующий хост ftp, имя пользователя и пароль. Я поместил файл на свой тестовый ftp-сайт в папку test / и назвал файл test.xml. Тестовый файл должен состоять как минимум из нескольких строк и иметь кодировку utf-16 (это может быть необязательно, но поможет воссоздать мою точную ситуацию). Я использовал команду vim :set fileencoding=utf-16 после открытия нового файла и ввел текст xml, указанный выше. Наконец, чтобы бежать, просто сделай

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

Код:

(ПРИМЕЧАНИЕ: этот код изменен для использования настраиваемого объекта FTPClient, ссылка на который приведена ниже в разделе «РЕДАКТИРОВАТЬ 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;
    }

}

РЕДАКТИРОВАТЬ 2:

Хорошо, я последовал совету CheckedXputStream и вот мои результаты. Я сделал копию FTPClient apache под названием MyFTPClient и заключил SocketInputStream и BufferedInputStream в CheckedInputStream, используя CRC32 контрольные суммы. Кроме того, я обернул FileOutputStream, который передаю FTPClient, чтобы сохранить результат в CheckOutputStream с CRC32 контрольной суммой. Код для MyFTPClient размещен здесь, и я изменил приведенный выше тестовый код, чтобы использовать эту версию FTPClient. (попытался опубликовать основной URL-адрес в измененный код, но мне нужно 10 очков репутации, чтобы опубликовать более одного URL-адреса!), test.xml и test.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

В этом нет никакого смысла, потому что здесь md5-суммы соответствующих файлов:

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

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

Я в растерянности. Я клянусь, что я не переставлял имена файлов / пути на любом этапе этого процесса, и я трижды проверял каждый шаг. Это должно быть что-то простое, но я не имею ни малейшего представления, где искать дальше. В интересах практичности я собираюсь продолжить, позвонив в оболочку, чтобы выполнить мои FTP-передачи, но я намерен продолжать это, пока не пойму, что, черт возьми, происходит. Я обновлю эту ветку своими выводами и буду благодарен за любой вклад, который может быть у кого-то. Надеюсь, когда-нибудь это будет кому-то полезно!


person Chris Suter    schedule 30.06.2010    source источник
comment
Вау, это странно. Я проверил исходный код BufferedInputStream и SocketInputStream (по крайней мере, часть Java) и не вижу ничего, что могло бы изменить байты таким образом. Я бы предложил сделать копию FTPClient и изменить иерархию входного потока на CheckedInputStream(BufferedInputStream(CheckedInputStream(SocketInputStream()))), а также использовать контрольные суммы, чтобы увидеть, можете ли вы определить, где изменяются байты. Это было бы полезной информацией в вопросе. (Еще лучше, разместите свой тестовый код в Интернете и сделайте ссылку на него)   -  person David Z    schedule 30.06.2010
comment
Также +1 за хорошо написанный вопрос ;-)   -  person David Z    schedule 30.06.2010
comment
Я попробую это; Спасибо. Я никогда не слышал о CheckedInputStream. Очень круто!!   -  person Chris Suter    schedule 30.06.2010
comment
Кто-нибудь еще (например, здесь) пытался воспроизвести это?   -  person Stephen C    schedule 30.06.2010


Ответы (3)


После входа на ftp-сервер

ftp.setFileType(FTP.BINARY_FILE_TYPE);

Следующая строка не решает эту проблему:

//ftp.setFileTransferMode(org.apache.commons.net.ftp.FTP.BINARY_FILE_TYPE);
person Sven    schedule 16.02.2011
comment
Спасибо, это помогло мне. Странно, что текстовый режим будет по умолчанию. - person Davor; 30.04.2015

Мне кажется, что в вашем коде приложения выбор режима ASCII и BINARY был инвертирован. ASCII проходит без изменений, BINARY, выполняющий перевод символов конца строки, является полной противоположностью тому, как должен работать FTP.

Если проблема не в этом, отредактируйте свой вопрос, чтобы добавить соответствующую часть кода.

ИЗМЕНИТЬ

Пара других возможных (но маловероятных ИМО) объяснений:

  • FTP-сервер сломан / неправильно настроен. (Можно ли успешно загрузить файл в режиме ASCII / BINARY с помощью FTP-утилиты командной строки, отличной от Java?)
  • Вы разговариваете с FTP-сервером через прокси-сервер, который сломан или неправильно настроен.
  • Вам каким-то образом удалось заполучить изворотливую (взломанную) копию JAR-файла клиента FTP Apache. (Да, да, очень маловероятно ...)
person Stephen C    schedule 30.06.2010
comment
Казалось бы, так, но я запускал код как минимум 5 раз и удалил как можно больше переменных. Я отредактировал свое сообщение, включив в него проверенный мной код, воссоздающий проблему. К сожалению, я не могу предложить ftp-сайт, с которого можно загрузить файл, поэтому, надеюсь, у вас есть к нему доступ (я просто тестирую на localhost). Спасибо за ответ, и я буду признателен, если у вас есть какие-либо мысли! - person Chris Suter; 30.06.2010
comment
Я рассмотрел первый случай, который вы упомянули, как наиболее вероятное объяснение правильности кода. Это довольно стандартная установка proftp в Ubuntu. Я просто попытался загрузить с помощью стандартного клиента командной строки ftp, и файл xml прошел нормально (этого, в некоторой степени, следовало ожидать, поскольку клиент, вероятно, использует режим ascii, который правильно передал xml с помощью FTPClient). Он также правильно передает mp3-файл (тот же md5sum), поэтому он не выглядит как сервер, если только FTPClient не подключается к нему с другими настройками, чем клиент строки cmd (возможность). - person Chris Suter; 30.06.2010
comment
Кроме того, я бы поддержал вас за вашу помощь, но у меня еще нет 15 очков репутации! :) - person Chris Suter; 30.06.2010

Я обнаружил, что Apache retrieveFile (...) иногда не работает, если размер файла превышает определенный предел. Чтобы преодолеть это, я бы вместо этого использовал функцию retrieveFileStream (). Перед загрузкой я установил правильный тип файла и установил режим PassiveMode.

Таким образом, код будет выглядеть так

    ....
    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();
    ....
person Vivek Kumar    schedule 15.05.2013