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