not - java ftp connection
Transferir binario sin procesar con apache commons-net FTPClient? (3)
ACTUALIZACIÓN: resuelto
Estaba llamando a FTPClient.setFileType()
antes de FTPClient.setFileType()
sesión, lo que provocó que el servidor FTP utilizara el modo predeterminado ( ASCII
) sin importar en qué lo configuré. El cliente, por otro lado, se comportaba como si el tipo de archivo hubiera sido configurado correctamente. BINARY
modo BINARY
ahora está funcionando exactamente como se desea, transportando el byte de byte de archivo en todos los casos. Todo lo que tenía que hacer era husmear un poco en wireshark y luego simular los comandos de FTP usando netcat para ver qué estaba pasando. ¿Por qué no pensé en eso hace dos días? ¡Gracias a todos por su ayuda!
Tengo un archivo xml, utf-16 codificado, que descargo de un sitio FTP utilizando el FTPClient de la biblioteca java commons-net-2.0 de apache. Ofrece soporte para dos modos de transferencia: ASCII_FILE_TYPE
y BINARY_FILE_TYPE
, la diferencia es que ASCII
reemplazará los separadores de línea con el separador de línea local apropiado ( ''/r/n''
o simplemente ''/n''
- en hexadecimal, 0x0d0a
o simplemente 0x0a
) . Mi problema es este: tengo un archivo de prueba, codificado para utf-16, que contiene lo siguiente:
<?xml version=''1.0'' encoding=''utf-16''?>
<data>
<blah>blah</blah>
</data>
Aquí está el hex:
0000000: 003c 003f 0078 006d 006c 0020 0076 0065 .<.?.xml .ve
0000010: 0072 0073 0069 006f 006e 003d 0027 0031 .rsion=.''.1
0000020: 002e 0030 0027 0020 0065 006e 0063 006f ...0.''. .enco
0000030: 0064 0069 006e 0067 003d 0027 0075 0074 .ding=.''.ut
0000040: 0066 002d 0031 0036 0027 003f 003e 000a .f.-.1.6.''.?.>..
0000050: 003c 0064 0061 0074 0061 003e 000a 0009 .<.data>....
0000060: 003c 0062 006c 0061 0068 003e 0062 006c .<.blah>.bl
0000070: 0061 0068 003c 002f 0062 006c 0061 0068 .ah<./.blah
0000080: 003e 000a 003c 002f 0064 0061 0074 0061 .>...<./.data
0000090: 003e 000a
.>..
Cuando uso el modo ASCII
para este archivo, se transfiere correctamente, byte por byte; el resultado tiene el mismo md5sum. Estupendo. Cuando uso el modo de transferencia BINARY
, que no se supone que haga nada más que mezclar bytes desde un InputStream
en un OutputStream
, el resultado es que las nuevas líneas ( 0x0a
) se convierten a carriage return + newline pairs ( 0x0d0a
). Aquí está el hex después de la transferencia binaria:
0000000: 003c 003f 0078 006d 006c 0020 0076 0065 .<.?.xml .ve
0000010: 0072 0073 0069 006f 006e 003d 0027 0031 .rsion=.''.1
0000020: 002e 0030 0027 0020 0065 006e 0063 006f ...0.''. .enco
0000030: 0064 0069 006e 0067 003d 0027 0075 0074 .ding=.''.ut
0000040: 0066 002d 0031 0036 0027 003f 003e 000d .f.-.1.6.''.?.>..
0000050: 0a00 3c00 6400 6100 7400 6100 3e00 0d0a ..<.data>...
0000060: 0009 003c 0062 006c 0061 0068 003e 0062 ...<.blah>.b
0000070: 006c 0061 0068 003c 002f 0062 006c 0061 .lah<./.bla
0000080: 0068 003e 000d 0a00 3c00 2f00 6400 6100 .h.>....<./.da
0000090: 7400 6100 3e00 0d0a
ta>...
No solo convierte los caracteres de nueva línea (lo que no debería), sino que no respeta la codificación de utf-16 (no es que esperara que supiera que debería ser, es solo un canal de FTP tonto). El resultado es ilegible sin procesamiento adicional para realinear los bytes. Solo usaría el modo ASCII
, pero mi aplicación también moverá datos binarios reales (archivos mp3 e imágenes jpeg) a través del mismo conducto. El uso del modo de transferencia BINARY
en estos archivos binarios también hace que se 0x0d
aleatorios en sus contenidos, que no se pueden eliminar de manera segura ya que los datos binarios a menudo contienen secuencias 0x0d0a
legítimas. Si utilizo el modo ASCII
en estos archivos, el FTPClient "inteligente" convierte estos 0x0d0a
en 0x0a
dejando el archivo incoherente sin importar lo que haga.
Supongo que mi (s) pregunta (s) es (son): ¿Alguien sabe de alguna buena biblioteca de FTP para Java que solo mueva los malditos bytes desde allí hasta aquí, o voy a tener que hackear Apache commons-net-2.0 y mantener mi propio código de cliente FTP solo para esta sencilla aplicación? ¿Alguien más ha lidiado con este extraño comportamiento? Cualquier sugerencia sera apreciada.
Comprobé el código fuente de commons-net y no parece que sea responsable del comportamiento extraño cuando se usa el modo BINARY
. Pero el InputStream
que está leyendo en modo BINARY
es solo un java.io.BufferedInptuStream
envuelto alrededor de un socket InputStream
. ¿Alguna vez estas secuencias Java de nivel más bajo hacen alguna manipulación de bytes extraña? Me sorprendería si lo hicieran, pero no veo qué más podría estar sucediendo aquí.
EDIT 1:
Aquí hay una pieza mínima de código que imita lo que estoy haciendo para descargar el archivo. Para compilar, solo hazlo
javac -classpath /path/to/commons-net-2.0.jar Main.java
Para ejecutar, necesitará directorios / tmp / ascii y / tmp / binary para el archivo para descargar, así como un sitio ftp configurado con el archivo que se encuentra en él. El código también deberá configurarse con el host ftp, el nombre de usuario y la contraseña apropiados. Puse el archivo en mi sitio ftp de prueba bajo la carpeta test / y llamé al archivo test.xml. El archivo de prueba debe tener al menos más de una línea y estar codificado en UTF-16 (esto puede no ser necesario, pero ayudará a recrear mi situación exacta). Utilicé el comando vim''s :set fileencoding=utf-16
después de abrir un nuevo archivo e ingresé el texto xml mencionado anteriormente. Finalmente, para correr, solo hazlo
java -cp .:/path/to/commons-net-2.0.jar Main
Código:
(NOTA: este código se modificó para usar un objeto FTPClient personalizado, vinculado a continuación debajo de "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;
}
}
EDICION 2:
De acuerdo, seguí el consejo de CheckedXputStream
y aquí están mis resultados. Hice una copia del FTPClient
de apache llamado MyFTPClient
, y envolví tanto el SocketInputStream
como el BufferedInputStream
en un CheckedInputStream
usando las sumas de comprobación CRC32
. Además, envolví el FileOutputStream
que doy a FTPClient
para almacenar el resultado en un CheckOutputStream
con suma de comprobación CRC32
. El código para MyFTPClient se publica aquí y modifiqué el código de prueba anterior para usar esta versión del FTPClient (traté de publicar una URL clave en el código modificado, pero necesito 10 puntos de reputación para publicar más de una URL), test.xml
y test.mp3
y los resultados fueron así:
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
Esto hace, básicamente, cero sentido alguno porque aquí están los md5sums de los archivos correspondientes:
bf89673ee7ca819961442062eaaf9c3f ascii/test.mp3
7bd0e8514f1b9ce5ebab91b8daa52c4b binary/test.mp3
ee172af5ed0204cf9546d176ae00a509 original/test.mp3
104e14b661f3e5dbde494a54334a6dd0 ascii/test.xml
36f482a709130b01d5cddab20a28a8e8 binary/test.xml
104e14b661f3e5dbde494a54334a6dd0 original/test.xml
Estoy perdido Juro que no he permutado los nombres de archivo / rutas en ningún punto de este proceso, y he verificado tres veces cada paso. Debe ser algo simple, pero no tengo la más remota idea de dónde mirar a continuación. En aras de la practicidad, voy a proceder llamando al intérprete de comandos para hacer mis transferencias FTP, pero tengo la intención de seguir hasta que entienda qué demonios está pasando. Actualizaré este hilo con mis hallazgos, y continuaré agradeciendo cualquier contribución que pueda tener alguien. ¡Espero que esto sea útil para alguien en algún momento!
Me parece que el código de la aplicación podría haber invertido la selección del modo ASCII y BINARIO. ASCII viene sin cambios, BINARY realiza traducciones de fin de línea es exactamente lo opuesto de cómo se supone que FTP funciona.
Si ese no es el problema, edite su pregunta para agregar la parte relevante de su código.
EDITAR
Un par de otras posibles explicaciones (pero IMO improbable):
- El servidor FTP está roto / mal configurado. (¿Se puede descargar con éxito el archivo en modo ASCII / BINARY utilizando una utilidad FTP de línea de comandos que no sea Java?)
- Está hablando con el servidor FTP a través de un proxy que está roto o mal configurado.
- De alguna manera has logrado obtener una copia poco fiable (pirateada) del archivo JAR del cliente Apache FTP. (Sí, sí, muy poco probable ...)
Después de iniciar sesión en el servidor ftp
ftp.setFileType(FTP.BINARY_FILE_TYPE);
La siguiente línea no lo resuelve:
//ftp.setFileTransferMode(org.apache.commons.net.ftp.FTP.BINARY_FILE_TYPE);
Descubrí que Apache retrieveFile (...) a veces no funcionaba con File Sizes excediendo un cierto límite. Para superar eso, usaría retrieveFileStream () en su lugar. Antes de la descarga, configuré el tipo de archivo correcto y establecí el modo en PassiveMode
Entonces el código se verá
....
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();
....