Подтвердить что ты не робот

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

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

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

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

Здесь 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                                            ;                         &nbsp         .>..

Когда я использую режим ASCII для этого файла, он передает правильно, byte-for-byte; результат имеет тот же самый md5sum. Отлично. Когда я использую режим передачи BINARY, который не должен ничего делать, кроме как перетасовывать байты из InputStream в OutputStream, результат состоит в том, что строки новой строки (0x0a) преобразуются в пары возврата каретки + новой строки (0x0d0a). Здесь 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 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                       &nbsp                                   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.xml. Файл теста должен содержать не более одной строки и быть закодирован в utf-16 (это может быть необязательно, но поможет воссоздать точную ситуацию). Я использовал команду vim :set fileencoding=utf-16 после открытия нового файла и ввел текст xml, упомянутый выше. Наконец, чтобы запустить, просто сделайте

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

код:

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

}

ИЗМЕНИТЬ 2:

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

4b9b3361

Ответ 1

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

ftp.setFileType(FTP.BINARY_FILE_TYPE);

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

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

Ответ 2

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

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

ИЗМЕНИТЬ

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

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

Ответ 3

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