Java’s FTPClient from Apache Commons Net is your go-to for talking FTP, but it’s not just a simple file copy.
Let’s watch it in action. Imagine we have a remote FTP server at ftp.example.com with a user myuser and password mypass. We want to upload a local file named local_data.txt to the /upload/ directory on the server.
import org.apache.commons.net.ftp.FTPClient;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.SocketException;
public class FtpUploader {
public static void main(String[] args) {
FTPClient client = new FTPClient();
String server = "ftp.example.com";
int port = 21; // Standard FTP port
String user = "myuser";
String pass = "mypass";
String localFilePath = "local_data.txt";
String remoteDirectory = "/upload/";
String remoteFileName = "remote_data.txt";
try {
client.connect(server, port);
client.login(user, pass);
client.enterLocalPassiveMode(); // Crucial for firewalls
// Ensure the remote directory exists
if (!client.changeWorkingDirectory(remoteDirectory)) {
System.err.println("Remote directory " + remoteDirectory + " does not exist or cannot be accessed.");
// Optionally create it: client.makeDirectory(remoteDirectory);
// and then changeWorkingDirectory again.
return;
}
client.setFileType(FTP.BINARY_FILE_TYPE); // Set to binary for most files
try (InputStream localFileStream = new FileInputStream(localFilePath);
OutputStream remoteFileStream = client.storeFileStream(remoteFileName)) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = localFileStream.read(buffer)) != -1) {
remoteFileStream.write(buffer, 0, bytesRead);
}
remoteFileStream.flush(); // Ensure all data is written
}
boolean success = client.completePendingCommand(); // Essential to finalize upload
if (success) {
System.out.println("File uploaded successfully!");
} else {
System.err.println("File upload failed.");
}
} catch (SocketException e) {
System.err.println("Error connecting to FTP server: " + e.getMessage());
} catch (IOException e) {
System.err.println("FTP operation failed: " + e.getMessage());
} finally {
try {
if (client.isConnected()) {
client.logout();
client.disconnect();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
This code connects, logs in, and then uses storeFileStream to get an OutputStream to the remote file. You write your local file’s content into this stream, and FTPClient handles sending it over. The completePendingCommand() is vital – it tells the server the transfer is done and checks for success.
The core problem FTPClient solves is abstracting the FTP protocol. Instead of manually crafting USER, PASS, CWD, STOR, and PORT/PASV commands, you use Java methods. It manages the control connection for commands and the data connection for file transfers. You choose between active and passive modes. Active mode means the server connects back to your client on a random port for data. Passive mode means your client connects to a port specified by the server for data. Passive is almost always preferred because it plays nicer with firewalls and NAT.
The enterLocalPassiveMode() call is the key switch for this. Without it, you’re likely to hit connection refused errors if any network devices are between your client and the server. When you call storeFileStream or retrieveFileStream, the client initiates the data connection after the server responds to the PASV command with an IP and port.
When you call client.storeFileStream(remoteFileName), the FTPClient sends the STOR remoteFileName command. The server responds with a 150 (File status okay; about to open data connection) or 125 (Data connection already open; transfer starting) message, and importantly, includes the IP address and port for the data connection in its response if using passive mode. Your FTPClient then establishes a new socket connection to that IP and port to send the file data. Once your OutputStream is closed and you call client.completePendingCommand(), the client sends the QUIT command (or other appropriate command to close the data connection and get the final status) and waits for the server’s 226 (Closing data connection) or 225 (Transfer complete) message.
The single most surprising thing about FTP is how its state is managed. Each command on the control connection can trigger a separate data connection, and the client and server must agree on the mode (active/passive) and port for every single file transfer. This is why enterLocalPassiveMode() is so critical and why completePendingCommand() is not optional; it’s the handshake that confirms the transfer is finished and the server has acknowledged it.
The next hurdle is handling different file types and error codes beyond the basic success/failure.