Передача необработанных двоичных файлов с помощью apache commons-net - PullRequest
27 голосов
/ 30 июня 2010

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

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

У меня есть xml-файл в кодировке utf-16, который я загружаю с FTP-сайта, используя FTPClient java-библиотеки apache commons-net-2.0. Он предлагает поддержку двух режимов передачи: 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
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 s, которые невозможно безопасно удалить, поскольку двоичные данные часто содержат допустимые последовательности 0x0d0a. Если я использую режим ASCII для этих файлов, то «умный» FTPClient преобразует эти 0x0d0a s в 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

Это имеет, по сути, нулевой смысл, поскольку здесь приведены md5sums соответствующих файлов:

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-передач, но я намерен продолжать это, пока не пойму, что, черт возьми, происходит.Я буду обновлять эту ветку с моими выводами, и я буду продолжать ценить любой вклад, который любой может иметь.Надеюсь, в какой-то момент это кому-нибудь пригодится!

Ответы [ 3 ]

32 голосов
/ 16 февраля 2011

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

ftp.setFileType(FTP.BINARY_FILE_TYPE);

Строка ниже не решает:

//ftp.setFileTransferMode(org.apache.commons.net.ftp.FTP.BINARY_FILE_TYPE);
4 голосов
/ 30 июня 2010

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

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

EDIT

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

  • FTP-сервер неисправен / неправильно настроен. (Можете ли вы успешно загрузить файл в режиме ASCII / BINARY, используя утилиту FTP для командной строки, отличную от Java?)
  • Вы общаетесь с FTP-сервером через прокси, который поврежден или неправильно настроен.
  • Вам как-то удалось заполучить хитрую (взломанную) копию файла JAR клиента FTP-клиента Apache. (Да, да, очень маловероятно ...)
3 голосов
/ 15 мая 2013

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

, поэтому код будет выглядеть так:

...