/*********************************************************************************
* TotalCross Software Development Kit *
* Copyright (C) 2000-2012 SuperWaba Ltda. *
* All Rights Reserved *
* *
* This library and virtual machine is distributed in the hope that it will *
* be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. *
* *
* This file is covered by the GNU LESSER GENERAL PUBLIC LICENSE VERSION 3.0 *
* A copy of this license is located in file license.txt at the root of this *
* SDK or can be downloaded here: *
* http://www.gnu.org/licenses/lgpl-3.0.txt *
* *
*********************************************************************************/
package totalcross.net;
import totalcross.io.IOException;
import totalcross.io.LineReader;
import totalcross.io.Stream;
import totalcross.sys.*;
import totalcross.ui.*;
import totalcross.util.Vector;
/**
* This class implements the File Transfer Protocol.
* <p>
* Note: if you're experiencing slowness in the server, try to disable the reverse-ip resolution in your FTP server:
* this will speedup everything.
* </p>
* Check the sample RemoteExplorer. Here is an example code:
*
* <pre>
  ftp = new FTP(url, user, pass, cbLog);
  ftp.makeDir("tempdir");
  ftp.changeDir("tempdir");
  String textfile = "Viva Verinha!";
  ByteArrayStream bas = new ByteArrayStream(textfile.getBytes());
  ftp.sendFile(bas, "verinha.txt", false);
  String[] files = ftp.list("*.txt");
  if (files == null || files.length != 1)
  cbLog.add("Something went wrong in sending files...");
  ftp.rename("verinha.txt", "vivaverinha.txt");
  bas = new ByteArrayStream(50);
  ftp.receiveFile("vivaverinha.txt", bas);
  cbLog.add(new String(bas.getCopy()));
  ftp.delete("vivaverinha.txt");
  ftp.changeDir("..");
  ftp.removeDir("tempdir");
* </pre>
*
* You can use the CompressedByteArrayStream to transfer and receive big files to/from the server (check the class for more
* information and samples).
* <p>
* To transfer a File to the server, all you have to do is:
*
* <pre>
  File f = new File("michelle.txt", File.READ_WRITE);
  ftp.sendFile(f, "michelle.txt", false); // last parameter depends on what you want to do
* </pre>
*
* It is currently impossible to transfer a whole PDBFile to/from the server. The solution for this would be to send
* each record in pieces, and store it in separate files in the server. Then a routine written in TotalCross in the server could
* reassemble the records into a new PDBFile. Here is an idea:
*
* <pre>
  // for the Client:
  // i assume that a ftp class is prepared to be used
  String name = "myPDBFile";
  PDBFile cat = new PDBFile(name + ".crtr.type", READ_WRITE);
  int n = cat.getRecordCount();
  for (int i = 0; i < n; i++)
  {
  if (!cat.setRecordPos(i))
  throw new RuntimeException("PDBFile is in use elsewhere!");
  else
  ftp.sendFile(cat, name + "#" + i, false);
  }
  // for the server
  String name = "myPDBFile";
  // for simplicity, I'll assume that the PDBFile does not exists
  byte[] buf = new byte[65536]; // in desktop this is possible
  PDBFile cat = new PDBFile(name + ".crtr.type", PDBFile.CREATE);
  for (int i = 0;; i++)
  {
  File f = new File(name + "#" + i, File.READ_WRITE);
  if (!f.exists())
  break; // no more records
  int size = f.getSize();
  f.readBytes(buf, 0, size);
  f.delete(); // could be also: f.close();
  cat.addRecord(size);
  cat.writeBytes(buf, 0, size);
  }
  cat.close();
* </pre>
*
* The example above can be easily changed to add support for compression.
* <p>
* Here is a list of error codes that can thrown if an Exception occurs: <p>
<TABLE width="100%" border=0 cellpadding="2" cellspacing="1" bgcolor="#FFFFFF">
<TR bgcolor="#CCCCCC" align="CENTER">
<TD><B>Code</B></TD>
<TD><B>Description</B></TD>
</TR>
<TR bgcolor="#FFFFCC">
<TD align=CENTER NOWRAP><B>100 Codes</B></TD>
<TD align=left><B>The requested action is being taken. Expect a reply
before proceeding with a new command.</B></TD>
</TR>
<TR VALIGN="TOP">
<TD align="CENTER" class="text1"><B>110</B></TD>
<TD>Restart marker reply.</TD>
</TR>
<TR VALIGN="TOP">
<TD align="CENTER" class="text1"><B>120</B></TD>
<TD>Service ready in (n) minutes.</TD>
</TR>
<TR VALIGN="TOP">
<TD align="CENTER" class="text1"><B>125</B></TD>
<TD>Data connection already open, transfer starting.</TD>
</TR>
<TR VALIGN="TOP">
<TD align="CENTER" class="text1"><B>150</B></TD>
<TD>File status okay, about to open data connection.</TD>
</TR>
<TR VALIGN="TOP" bgcolor="#FFFFCC">
<TD align=CENTER><B>200 Codes</B></TD>
<TD align=left><B>The requested action has been successfully completed.</B></TD>
</TR>
<TR VALIGN="TOP">
<TD align="CENTER" class="text1"><B>200</B></TD>
<TD>Command okay.</TD>
</TR>
<TR VALIGN="TOP">
<TD align="CENTER" class="text1"><B>202</B></TD>
<TD>Command not implemented</TD>
</TR>
<TR VALIGN="TOP">
<TD align="CENTER" class="text1"><B>211</B></TD>
<TD>System status, or system help reply.</TD>
</TR>
<TR VALIGN="TOP">
<TD align="CENTER" class="text1"><B>212</B></TD>
<TD>Directory status.</TD>
</TR>
<TR VALIGN="TOP">
<TD align="CENTER" class="text1"><B>213</B></TD>
<TD>File status.</TD>
</TR>
<TR VALIGN="TOP">
<TD align="CENTER" class="text1"><B>214</B></TD>
<TD>Help message.</TD>
</TR>
<TR VALIGN="TOP">
<TD align="CENTER" class="text1"><B>215</B></TD>
<TD>NAME system type. (NAME is an official system name from the list
in the Assigned Numbers document.)</TD>
</TR>
<TR VALIGN="TOP">
<TD align="CENTER" class="text1"><B>220</B></TD>
<TD>Service ready for new user.</TD>
</TR>
<TR VALIGN="TOP">
<TD align="CENTER" class="text1"><B>221</B></TD>
<TD>Service closing control connection. (Logged out if appropriate.)</TD>
</TR>
<TR VALIGN="TOP">
<TD align="CENTER" class="text1"><B>225</B></TD>
<TD>Data connection open, no transfer in progress.</TD>
</TR>
<TR VALIGN="TOP">
<TD align="CENTER" class="text1"><B>226</B></TD>
<TD>Closing data connection. Requested file action successful (file
transfer, abort, etc.).</TD>
</TR>
<TR VALIGN="TOP">
<TD align="CENTER" class="text1"><B>227</B></TD>
<TD>Entering Passive Mode</TD>
</TR>
<TR VALIGN="TOP">
<TD align="CENTER" class="text1"><B>230</B></TD>
<TD>User logged in, proceed.</TD>
</TR>
<TR VALIGN="TOP">
<TD align="CENTER" class="text1"><B>250</B></TD>
<TD>Requested file action okay, completed.</TD>
</TR>
<TR VALIGN="TOP">
<TD align="CENTER" class="text1"><B>257</B></TD>
<TD>"PATHNAME" created.</TD>
</TR>
<TR VALIGN="TOP" bgcolor="#FFFFCC">
<TD align=CENTER><B>300 Codes</B></TD>
<TD align=left><B>The command has been accepted, but the requested
action is being held pending receipt of further information.</B></TD>
</TR>
<TR VALIGN="TOP">
<TD align="CENTER" class="text1"><B>331</B></TD>
<TD>User name okay, need password.</TD>
</TR>
<TR VALIGN="TOP">
<TD align="CENTER" class="text1"><B>332</B></TD>
<TD>Need account for login.</TD>
</TR>
<TR VALIGN="TOP">
<TD align="CENTER" class="text1"><B>350</B></TD>
<TD>Requested file action pending further information. <br>If you're using "list *.txt", try "list *.*" and filter localy.</TD>
</TR>
<TR VALIGN="TOP" bgcolor="#FFFFCC">
<TD align=CENTER><B>400 Codes</B></TD>
<TD align=left><B>The command was not accepted and the requested action
did not take place. <BR>
Tthe error condition is temporary, however, and the action may be
requested again.</B></TD>
</TR>
<TR VALIGN="TOP">
<TD align="CENTER" class="text1"><B>421</B></TD>
<TD>Service not available, closing control connection. (May be a reply
to any command if the service knows it must shut down.)'</TD>
</TR>
<TR VALIGN="TOP">
<TD align="CENTER" class="text1"><B>425</B></TD>
<TD>Can't open data connection.</TD>
</TR>
<TR VALIGN="TOP">
<TD align="CENTER" class="text1"><B>426</B></TD>
<TD>Connection closed, transfer aborted.</TD>
</TR>
<TR VALIGN="TOP">
<TD align="CENTER" class="text1"><B>450</B></TD>
<TD>Requested file action not taken. File unavailable (e.g., file busy).</TD>
</TR>
<TR VALIGN="TOP">
<TD align="CENTER" class="text1"><B>451</B></TD>
<TD>Requested action aborted, local error in processing.</TD>
</TR>
<TR VALIGN="TOP">
<TD align="CENTER" class="text1"><B>452</B></TD>
<TD>Requested action not taken. Insufficient storage space in system.</TD>
</TR>
<TR VALIGN="TOP" bgcolor="#FFFFCC">
<TD align=CENTER><B>500 Codes</B></TD>
<TD align=left><B>The command was not accepted and the requested action
did not take place.</B></TD>
</TR>
<TR VALIGN="TOP">
<TD align="CENTER" class="text1"><B>500</B></TD>
<TD>Syntax error, command unrecognized. This may include errors such
as command line too long.</TD>
</TR>
<TR VALIGN="TOP">
<TD align="CENTER" class="text1"><B>501</B></TD>
<TD>Syntax error in parameters or arguments.</TD>
</TR>
<TR VALIGN="TOP">
<TD align="CENTER" class="text1"><B>502</B></TD>
<TD>Command not implemented.</TD>
</TR>
<TR VALIGN="TOP">
<TD align="CENTER" class="text1"><B>503</B></TD>
<TD>Bad sequence of commands.</TD>
</TR>
<TR VALIGN="TOP">
<TD align="CENTER" class="text1"><B>504</B></TD>
<TD>Command not implemented for that parameter.</TD>
</TR>
<TR VALIGN="TOP">
<TD align="CENTER" class="text1"><B>530</B></TD>
<TD>User not logged in.</TD>
</TR>
<TR VALIGN="TOP">
<TD align="CENTER" class="text1"><B>532</B></TD>
<TD>Need account for storing files.</TD>
</TR>
<TR VALIGN="TOP">
<TD align="CENTER" class="text1"><B>550</B></TD>
<TD>Requested action not taken. File unavailable (e.g., file not found,
no access).</TD>
</TR>
<TR VALIGN="TOP">
<TD align="CENTER" class="text1"><B>552</B></TD>
<TD>Requested file action aborted, storage allocation exceeded</TD>
</TR>
<TR VALIGN="TOP">
<TD align="CENTER" class="text1"><B>553</B></TD>
<TD>Requested action not taken. Illegal file name.</TD>
</TR>
</TABLE>
*
* @since SuperWaba 5.6
* @author Guilherme C. Hazan
*/
public class FTP
{
/** Assign the progressInfo member to an instance of this interface to receive information of how many bytes were transfered.
* Note that there's no way to get the total size of the file, but only what has been transfered so far.
* @see #progressInfo
*/
public static interface ProgressInformation
{
/** Called to report the size of the file that will be transferred.
* You can use it to compute the transfer's percentage.
* @since TotalCross 1.2
*/
public void reportSize(int size); // guich@tc120_40
/** Called to inform how many bytes were transfered. */
public void transfering(int current);
}
private static final byte WAITING_FOR_REPLY = 1;
private static final byte COMPLETED = 2;
private static final byte WAITING_FOR_MORE_INFO = 3;
// private static final byte NOT_PROCESSED = 4;
// private static final byte NOT_ACCEPTED = 5;
/** Default port: 21. */
public static final int defaultPort = 21;
/** Default open timeout: 15 seconds. */
public static final int defaultOpenTimeout = 15000;
/** Default read timeout: 5 seconds. */
public static final int defaultReadTimeout = 5000;
/** Default write timeout: 5 seconds. */
public static final int defaultWriteTimeout = 5000;
/** Defines an ASCII transfer type.
* @see #setType(String)
*/
public static final String ASCII = "A";
/** Defines a BINARY transfer type.
* @see #setType(String)
*/
public static final String BINARY = "I";
/** Defines an EBCDIC transfer type.
* @see #setType(String)
*/
public static final String EBCDIC = "E";
private StringBuffer sbsc = new StringBuffer(100); // guich@562_4: now we use a string buffer to avoid gc
private byte buffer[];
private int openTimeout = defaultOpenTimeout;
private int readTimeout = defaultReadTimeout;
private int writeTimeout = defaultWriteTimeout;
private ComboBox cbLog;
private ListBox lbLog;
/** Assign the ProgressInformation member to receive information about the file transfer.
* This way, you can show it in a ProgressBar.
* @since TotalCross 1.14
*/
public ProgressInformation progressInfo;
/**
* This makes a sleep during the send of a file. Important: when using softick, you must set this to 500(ms) or more,
* or softick will starve to death.
*/
public int sendSleep;
/**
* Set this to true to send the log to both the console and the combo.
*
* @since SuperWaba 5.66
*/
public static boolean log2console = Settings.onJavaSE; // guich@566_26
private Socket myTCPConnection;
private LineReader myTCPConnectionReader; //flsobral@tc123_54: FTP now uses LineReader to process the server responses - greatly improving its performance.
private LineReader listReader; // used only for list and listNames.
/**
* Opens a socket to the given URL, user, password and the default timeouts.
* @param url The url as an ip or a full address to the server. The connection port is always 21
* @param user The user name for login
* @param pass The password for login
*
* @see #FTP(String, String, String, int, int, int, int, Control)
*/
public FTP(String url, String user, String pass) throws UnknownHostException, IOException, FTPConnectionClosedException
{
this(url, user, pass, defaultPort, defaultOpenTimeout, defaultReadTimeout, defaultWriteTimeout, null);
}
/**
* Opens a socket to the given URL, user, password, port and the default timeouts.
*
* @param url The url as an ip or a full address to the server.
* @param user The user name for login
* @param pass The password for login
* @param port The port used to open the connection.
*
* @see #FTP(String, String, String, int, int, int, int, Control)
*/
public FTP(String url, String user, String pass, int port) throws UnknownHostException, IOException, FTPConnectionClosedException
{
this(url, user, pass, port, defaultOpenTimeout, defaultReadTimeout, defaultWriteTimeout, null);
}
/**
* Opens a socket to the given URL, user, password, logging control and the default timeouts.
*
* @param url The url as an ip or a full address to the server. The connection port is always 21
* @param user The user name for login
* @param pass The password for login
* @param loggingControl The ListBox or ComboBox where the logging will be sent to.
*
* @see #FTP(String, String, String, int, int, int, int, Control)
*/
public FTP(String url, String user, String pass, Control loggingControl) throws UnknownHostException, IOException, FTPConnectionClosedException
{
this(url, user, pass, defaultPort, defaultOpenTimeout, defaultReadTimeout, defaultWriteTimeout, loggingControl);
}
/**
* Opens a socket to the given URL, user, password, and timeouts.
*
* @param url The url as an ip or a full address to the server. The connection port is always 21
* @param user The user name for login
* @param pass The password for login
* @param openTimeout The timeout used for the socket open
* @param readTimeout The timeout used for read operations
* @param writeTimeout The timeout used for write operations
*
* @see #FTP(String, String, String, int, int, int, int, Control)
*/
public FTP(String url, String user, String pass, int openTimeout, int readTimeout, int writeTimeout) throws UnknownHostException, IOException, FTPConnectionClosedException
{
this(url, user, pass, defaultPort, openTimeout, readTimeout, writeTimeout, null);
}
/**
* Opens a socket to the given URL, user, password, timeouts and logging control
*
* @param url The url as an ip or a full address to the server. The connection port is always 21
* @param user The user name for login
* @param pass The password for login
* @param openTimeout The timeout used for the socket open
* @param readTimeout The timeout used for read operations
* @param writeTimeout The timeout used for write operations
* @param loggingControl The ListBox or ComboBox where the logging will be sent to.
*
* @see #FTP(String, String, String, int, int, int, int, Control)
*/
public FTP(String url, String user, String pass, int openTimeout, int readTimeout, int writeTimeout, Control loggingControl) throws UnknownHostException, IOException, FTPConnectionClosedException
{
this(url, user, pass, defaultPort, openTimeout, readTimeout, writeTimeout, loggingControl);
}
/**
* Opens a socket to the given URL, user, password, port, timeouts and logging control.
*
* @param url The url as an ip or a full address to the server.
* @param user The user name for login
* @param pass The password for login
* @param port The port used to open the connection.
* @param openTimeout The timeout used for the socket open
* @param readTimeout The timeout used for read operations
* @param writeTimeout The timeout used for write operations
* @param loggingControl The ListBox or ComboBox where the logging will be sent to.
*/
public FTP(String url, String user, String pass, int port, int openTimeout, int readTimeout, int writeTimeout, Control loggingControl) throws UnknownHostException, IOException, FTPConnectionClosedException
{
setLoggingControl(loggingControl);
try
{
myTCPConnection = new Socket(url, port, openTimeout, true); // guich@561_14: use for Palm OS
}
catch (totalcross.net.UnknownHostException e)
{
log("Not connected. Error: " + e.getMessage());
throw e;
}
catch (totalcross.io.IOException e)
{
log("Not connected. Error: " + e.getMessage());
throw e;
}
log("Connected.");
myTCPConnectionReader = new LineReader(myTCPConnection);
myTCPConnection.readTimeout = readTimeout; // guich@570_16
myTCPConnection.writeTimeout = writeTimeout;
this.openTimeout = openTimeout;
this.readTimeout = readTimeout;
checkResponse(COMPLETED);
sendCommand("USER ", user, WAITING_FOR_MORE_INFO);
sendCommand("PASS ", pass, COMPLETED);
}
/**
* Sends a quit command to the server and closes the socket.
*
* @throws FTPConnectionClosedException
* @throws totalcross.io.IOException
*/
public void quit() throws FTPConnectionClosedException, totalcross.io.IOException
{
if (myTCPConnection != null)
{
try
{
sendCommand("QUIT", null, COMPLETED);
}
catch (totalcross.io.IOException e)
{
// The server might close the connection without sending an acknowledgement,
// so we catch and ignore IOExceptions at this point.
}
finally
{
myTCPConnection.close();
myTCPConnection = null;
myTCPConnectionReader = null;
}
}
}
/**
* Returns the name of the current working directory.
*
* @return The name of the current working directory.
* @throws FTPConnectionClosedException
* @throws totalcross.io.IOException
*/
public String getCurrentDir() throws FTPConnectionClosedException, totalcross.io.IOException
{
String s = sendCommand("PWD", null, COMPLETED);
return s.substring(s.indexOf('"') + 1, s.lastIndexOf('"'));
}
/**
* Changes the user's current working directory to the given one.
*
* This command allows the user to work with a different directory or dataset for file storage or retrieval without
* altering his login or accounting information. Transfer parameters are similarly unchanged. The argument is a
* pathname specifying a directory or other system dependent file group designator.
*
* @param pathName Specifies a directory or other system dependent file group designator.
*/
public void setCurrentDir(String pathName) throws FTPConnectionClosedException, totalcross.io.IOException
{
sendCommand("CWD ", pathName, COMPLETED);
}
/**
* Sets the data representation type.
*
* @param type specifies the data representation type.
* @throws NullPointerException if type is null.
* @see #ASCII
* @see #BINARY
* @see #EBCDIC
*/
public void setType(String type) throws FTPConnectionClosedException, totalcross.io.IOException
{
if (type == null)
throw new NullPointerException("Argument 'type' cannot have a null value.");
sendCommand("TYPE ", type, COMPLETED);
}
/**
* The argument is a HOST-PORT specification for the data port to be used in data connection. There are defaults for
* both the user and server data ports, and under normal circumstances this command and its reply are not needed. If
* this command is used, the argument is the concatenation of a 32-bit internet host address and a 16-bit TCP port
* address. This address information is broken into 8-bit fields and the value of each field is transmitted as a
* decimal number (in character string representation). The fields are separated by commas. A port command would be:
* <pre>
* PORT h1,h2,h3,h4,p1,p2
* </pre>
* where h1 is the high order 8 bits of the internet host address.
*
* @param port port to be used in data connection
* @throws NullPointerException if port is null.
*/
public void setPort(String port) throws FTPConnectionClosedException, totalcross.io.IOException
{
if (port == null)
throw new NullPointerException("Argument 'port' cannot have a null value.");
sendCommand("PORT ", port, COMPLETED);
}
/**
* Deletes the file specified at the server site.
*
* @param pathName the file to be deleted.
* @throws FTPConnectionClosedException
* @throws totalcross.io.IOException
* @throws NullPointerException
* if pathName is null.
*/
public void delete(String pathName) throws FTPConnectionClosedException, totalcross.io.IOException
{
if (pathName == null)
throw new NullPointerException("Argument 'pathName' cannot have a null value.");
sendCommand("DELE ", pathName, COMPLETED);
}
/**
* Creates a directory at the server site.
*
* This command causes the directory specified in the pathname to be created as a directory (if the pathname is
* absolute) or as a subdirectory of the current working directory (if the pathname is relative).
*
* @param pathName Specifies the directory to be created.
* @throws NullPointerException if pathName is null.
*/
public void createDir(String pathName) throws FTPConnectionClosedException, totalcross.io.IOException
{
if (pathName == null)
throw new NullPointerException("Argument 'pathName' cannot have a null value");
sendCommand("MKD ", pathName, COMPLETED);
}
/**
* Causes the directory specified in the pathname to be removed as a directory (if the pathname is absolute) or as a
* subdirectory of the current working directory (if the pathname is relative).
*
* @param pathName Specifies the directory to be removed.
* @throws NullPointerException if pathName is null.
*/
public void removeDir(String pathName) throws FTPConnectionClosedException, totalcross.io.IOException
{
if (pathName == null)
throw new NullPointerException("Argument 'pathName' cannot have a null value");
sendCommand("RMD ", pathName, COMPLETED);
}
/**
* This command may help to keep the connection open.
*
* This command does not affect any parameters or previously entered commands. It specifies no action other than that
* the server send an OK reply.
*/
public void noop() throws FTPConnectionClosedException, totalcross.io.IOException
{
sendCommand("NOOP", null, COMPLETED);
}
/**
* Renames a file at the server site.
*
* @param oldPathName Old pathname of the file which is to be renamed.
* @param newPathName New pathname of the file specified to be renamed.
* @throws NullPointerException if any of the arguments, oldPathName and newPathName, is null.
*/
public void rename(String oldPathName, String newPathName) throws FTPConnectionClosedException, totalcross.io.IOException
{
if (oldPathName == null)
throw new NullPointerException("Argument 'oldPathName' cannot have a null value.");
if (newPathName == null)
throw new NullPointerException("Argument 'newPathName' cannot have a null value.");
sendCommand("RNFR ", oldPathName, WAITING_FOR_MORE_INFO);
sendCommand("RNTO ", newPathName, COMPLETED);
}
/**
* Sends a file to be stored at the server site. If the file specified in the pathname exists at the server site,
* then its contents shall be replaced by the data being transferred. A new file is created at the server site if the
* file specified in the pathname does not already exist.
*
* @param inputStream The Stream from where the data will be read (using readBytes method)
* @param pathName The name of the destination file in the server
* @return Returns the total bytes sent. Its up to the user to check if it was the desired amount.
* @throws NullPointerException if any of the arguments, inputStream or pathName, is null.
*/
public int sendFile(Stream inputStream, String pathName) throws FTPConnectionClosedException, totalcross.io.IOException
{
if (inputStream == null)
throw new NullPointerException("Argument 'inputStream' cannot have a null value.");
if (pathName == null)
throw new NullPointerException("Argument 'pathName' cannot have a null value.");
int count, total = 0;
setType(BINARY);
/*
* Important: linger must be set to true. in a gprs connection, the data is not completely sent until the
* socket.close is sent. In this case, we MUST wait for the acknowledge from the server, otherwise the file will
* not be flushed.
*/
Socket socket = openReturningPath(false);
sendCommand("STOR ", pathName, WAITING_FOR_REPLY);
log("0 bytes sent");
if (progressInfo != null) progressInfo.transfering(0);
int tries = 3;
if (buffer == null)
buffer = new byte[2048];
while ((count = inputStream.readBytes(buffer, 0, buffer.length)) > 0)
{
int totalSent = 0;
while (totalSent < count) // this may happen when using softick
{
int sent = socket.writeBytes(buffer, totalSent, count - totalSent);
if (sendSleep > 0)
Vm.sleep(sendSleep); // this is needed for softick
if (sent == 0 && tries-- == 0) // try to write up to 3 times
throw new totalcross.io.IOException("Cannot write to server.|Transmitted " + total + " bytes");
if (sent > 0)
totalSent += sent;
}
tries = 3;
total += totalSent;
logReplace(total + " bytes sent");
if (progressInfo != null) progressInfo.transfering(total);
}
log("Flushing file...");
try
{
socket.close();
// if the socket was correctly closed, we can safely ignore any timeout exceptions
log("Flushed. Receiving ack...");
try
{
checkResponse(COMPLETED);
}
catch (totalcross.io.IOException e)
{
String msg = e.getMessage();
if (msg != null && (msg.startsWith("No response") || msg.indexOf("450") >= 0))
{
/*
* This is a case where, given to the long wait for file flushing, the ack from the server was lost.
* Since the socket had closed fine, we can ensure that the file was transfered and just ignore the ack.
*/
log("Ack was lost, but file transfer succeed.");
}
else
throw e; // something else happened, dispatch the exception
}
}
catch (totalcross.io.IOException e)
{
// if the socket could not be closed, let the user handle
// the exception because the file was not transfered
checkResponse(COMPLETED);
}
return total;
}
/**
* Transfers a copy of the file specified in the pathname from the server. The status and contents of the file at the
* server site shall be unaffected.
*
* @param pathName Specifies the file to be retrieved.
* @param outputStream The stream to where the file will be written.
* @return number of bytes read.
* @throws NullPointerException if any of the arguments, pathName or outputStream, is null.
*/
public int receiveFile(String pathName, Stream outputStream) throws FTPConnectionClosedException, totalcross.io.IOException
{
if (pathName == null)
throw new NullPointerException("Argument 'pathName' cannot have a null value.");
if (outputStream == null)
throw new NullPointerException("Argument 'outputStream' cannot have a null value.");
setType(BINARY);
Socket socket = openReturningPath(true);
if (progressInfo != null) // guich@tc120_40
try
{
String fsize = sendCommand("SIZE ", pathName, COMPLETED);
progressInfo.reportSize(Convert.toInt(fsize.substring(fsize.lastIndexOf(' ')+1)));
}
catch (Exception e) {}
sendCommand("RETR ", pathName, WAITING_FOR_REPLY);
int count;
int total = 0;
log("0 bytes received");
if (progressInfo != null) progressInfo.transfering(0);
int tries = 3;
if (buffer == null)
buffer = new byte[2048];
while ((count = socket.readBytes(buffer)) >= 0) // guich@561_8: != -1 -> > 0
{
if (count > 0)
{
tries = 3;
total += count;
logReplace(total + " bytes received");
if (progressInfo != null) progressInfo.transfering(total);
outputStream.writeBytes(buffer, 0, count);
}
else if (tries-- == 0)
break;
}
try
{
checkResponse(COMPLETED); //flsobral@tc123_50: always check for the server response, even when count is -1 (EOF).
}
catch (totalcross.io.IOException e)
{
log("Cannot read from server.|Received " + total + " bytes. Error: " + e.getMessage());
throw e;
}
socket.close();
return total;
}
/**
* List the contents on the specified pathname.
*
* This command causes a list to be sent from the server passive DTP. If the pathname specifies a directory or other
* group of files, the server should transfer a list of files in the specified directory. If the pathname specifies a
* file then the server should send current information on the file. A null argument implies the user's current
* working or default directory. The data transfer is over the data connection in type ASCII or type EBCDIC. (The
* user must ensure that the TYPE is appropriately ASCII or EBCDIC). Since the information on a file may vary widely
* from system to system, this information may be hard to use automatically in a program, but may be quite useful to
* a human user.
*
* @param pathName Specifies a directory, a group of files or a single file. A null value lists the user's current
* directory or the default directory.
* @return The contents of the given pathname in the server
* @throws FTPConnectionClosedException
* @throws IOException
*/
public String[] list(String pathName) throws FTPConnectionClosedException, totalcross.io.IOException
{
Socket socket = openReturningPath(true);
sendCommand("LIST ", pathName, WAITING_FOR_REPLY);
Vector v = new Vector(50);
String s;
if (listReader == null)
listReader = new LineReader(socket);
else
listReader.setStream(socket);
while (true)
{
if ((s = listReader.readLine()) != null)
v.addElement(s);
else
break;
}
if (v.size() > 0 && v.items[v.size() - 1].toString().startsWith("total"))
v.removeElementAt(v.size() - 1); // remove the "total" information
socket.close();
checkResponse(COMPLETED);
return (String[]) v.toObjectArray();
}
/**
* List the names of the files on the specified pathname.
*
* This command causes a directory listing to be sent from server to user site. The pathname should specify a
* directory or other system-specific file group descriptor; a null argument implies the current directory. The
* server will return a stream of names of files and no other information. The data will be transferred in ASCII or
* EBCDIC type over the data connection as valid pathname strings separated by <CRLF> or
* <NL>. (The user must ensure that the TYPE is correct.) This command is intended to return information that can be
* used by a program to further process the files automatically. For example, in the implementation of a "multiple
* get" function.
*
* @param pathName Specifies a directory or a file group descriptor. A null value implies the user's current directory.
* @return The files of the given pathname in the server
* @throws FTPConnectionClosedException
* @throws totalcross.io.IOException
*/
public String[] listNames(String pathName) throws FTPConnectionClosedException, totalcross.io.IOException
{
Socket socket = openReturningPath(true);
sendCommand("NLST ", pathName, WAITING_FOR_REPLY);
Vector v = new Vector(50);
String s;
if (listReader == null)
listReader = new LineReader(socket);
else
listReader.setStream(socket);
while (true)
{
if ((s = listReader.readLine()) != null)
v.addElement(s);
else
break;
}
if (v.size() > 0 && v.items[v.size() - 1].toString().startsWith("total"))
v.removeElementAt(v.size() - 1); // remove the "total" information
socket.close();
checkResponse(COMPLETED);
return (String[]) v.toObjectArray();
}
/////////////////////////////////////////////////////////////////////////////////////////
private Socket openReturningPath(boolean linger) throws FTPConnectionClosedException, totalcross.net.UnknownHostException, totalcross.io.IOException
{
String line = sendCommand("PASV", "", COMPLETED); // 227 Entering Passive Mode (219,112,241,81,68,33)
// get the host and port
if (line == null || line.length() == 0)
throw new totalcross.io.IOException("Nothing read from PASV|connection closed?");
line = line.substring(line.indexOf('(') + 1);
line = line.substring(0, line.lastIndexOf(')')); // guich@561_6: some servers return "33)" and others "33)."
String[] p = Convert.tokenizeString(line, ',');
if (p == null || p.length != 6)
throw new totalcross.io.IOException("Error! Invalid answer: " + line);
String host = p[0] + '.' + p[1] + '.' + p[2] + '.' + p[3];
int port = 21;
try
{
port = (Convert.toInt(p[4]) << 8) | Convert.toInt(p[5]);
}
catch (InvalidNumberException ine)
{
log(ine.getMessage());
throw new totalcross.io.IOException("Invalid port number: " + p[4] + p[5]);
}
log("Connecting to " + host + ":" + port);
Socket socket = null;
for (int i = 0; i < 5; i++) // guich@561_8: try more than one time
{
try
{
socket = new Socket(host, port, openTimeout, linger); // guich@561_14: 15: use nolinger for Palm OS
socket.readTimeout = readTimeout;
socket.writeTimeout = writeTimeout;
break;
}
catch (totalcross.net.UnknownHostException e)
{
throw e;
}
catch (totalcross.io.IOException e)
{
if (i < 4)
Vm.sleep(500);
else
throw e; // flsobral@tc100b3: failed all 5 times, time to give up and throw the exception caught.
}
}
return socket;
}
private boolean isDigit(char c)
{
return '0' <= c && c <= '9';
}
private String readAck() throws totalcross.io.IOException
{
try
{
String s;
do
{
s = myTCPConnectionReader.readLine();
if (s == null)
break;
}
while (!isDigit(s.charAt(0)) || !isDigit(s.charAt(1)) || !isDigit(s.charAt(2)) || s.charAt(3) != ' ');
return s;
}
catch (ArrayIndexOutOfBoundsException aioobe)
{
return null;
}
}
private String checkResponse(byte expectedReturnType) throws FTPConnectionClosedException, totalcross.io.IOException
{
String line = readAck();
if (line != null)
{
int ack = -1;
try
{
ack = Convert.toInt(line.substring(0, 3));
}
catch (InvalidNumberException ine)
{
// This exception will NEVER be thrown by Conver.toInt because readAck() ALWAYS returns strings starting with 3 digits.
Vm.warning("FTP.checkResponse unexpected error: " + ine.getMessage());
}
int type = line.charAt(0) - '0';
log(line);
if (type == expectedReturnType)
return line;
if (ack == 421 || ack == 425 || ack == 426) // guich@561_8
{
myTCPConnection.close();
myTCPConnection = null;
throw new FTPConnectionClosedException("Connection timed out. This FTP|instance is no longer valid.");
}
throw new totalcross.io.IOException("Not acknowledged|Expected of type " + expectedReturnType + "|Received: "
+ ack);
}
// Cleans the socket's incoming buffer
if (buffer == null)
buffer = new byte[2048];
while (myTCPConnection.readBytes(buffer, 0, buffer.length) > 0)
;
myTCPConnectionReader.setStream(myTCPConnection);
throw new totalcross.io.IOException("No response|Maybe the ftp was closed?");
}
private String sendCommand(String s1, String s2, byte expectedReturnType) throws FTPConnectionClosedException, totalcross.io.IOException // guich@562_4: now the 2nd command is passed separately
{
if (s1 != null)
{
StringBuffer sb = sbsc;
sb.setLength(0);
sb.append(s1);
if (s2 != null)
sb.append(s2);
sb.append(Convert.CRLF); // guich@562_4: seems that we must send all the string at once
// Let's avoid a few method calls and call directly the writeBytes with 3 arguments.
byte[] sbBytes = Convert.getBytes(sb);
myTCPConnection.writeBytes(sbBytes, 0, sbBytes.length);
sb.setLength(sb.length() - 2); // remove the \r\n - we don't want to log it
log(s1.equals("PASS ") ? "PASS xxxxx" : sb.toString());
}
return checkResponse(expectedReturnType);
}
private void logReplace(String s)
{
// remove the last
if (cbLog != null)
cbLog.remove(cbLog.size() - 1);
else if (lbLog != null)
lbLog.remove(lbLog.size() - 1);
// and add the new
log(s);
}
private void log(String s)
{
if (s != null)
{
if (log2console)
Vm.debug(s);
// display in a combobox or listbox if any were assigned
if (cbLog != null)
{
cbLog.add(s);
cbLog.selectLast();
}
else if (lbLog != null)
{
lbLog.add(s);
lbLog.selectLast();
}
}
}
/**
* Sets the control where the log will be displayed. Only ComboBox and ListBox are allowed.
*/
public void setLoggingControl(Control c)
{
if (c == null) //rnovais@571_13: the control can be null
{
cbLog = null;
lbLog = null;
}
else if (c instanceof ComboBox)
cbLog = (ComboBox) c;
else if (c instanceof ListBox)
lbLog = (ListBox) c;
else
Vm.warning("Only ListBox and ComboBox are alowed in FTP.setLogControl");
}
/**
* Closes the control connection without issuing any quit or abort command to the server.
* Avoid using this method unless strictly necessary, use the quit method instead.
*/
public void forceClose() throws totalcross.io.IOException
{
myTCPConnection.close();
myTCPConnection = null;
myTCPConnectionReader = null;
}
}