Overview
Net::FTP implements the File Transfer Protocol client functionality in Ruby's standard library. The class provides methods for connecting to FTP servers, authenticating, navigating directories, and transferring files between local and remote systems.
Ruby's FTP implementation handles both active and passive connection modes, supports binary and ASCII transfer types, and manages the underlying TCP connections automatically. The Net::FTP class encapsulates the FTP protocol's command-response cycle, abstracting the low-level socket operations while exposing fine-grained control over transfer parameters.
The class follows Ruby's standard network library patterns, using blocks for automatic resource management and providing both synchronous operations and callback-based progress monitoring. Net::FTP maintains connection state internally, tracking the current directory, transfer mode, and authentication status.
require 'net/ftp'
# Basic FTP connection
ftp = Net::FTP.new('ftp.example.com')
ftp.login('username', 'password')
ftp.chdir('/pub/files')
files = ftp.list
ftp.close
Key classes include Net::FTP for client operations and Net::FTPError hierarchy for exception handling. The implementation supports standard FTP commands like LIST, RETR, STOR, CWD, and DELE, while handling protocol-specific details such as data connection establishment and response code interpretation.
# Block-based resource management
Net::FTP.open('ftp.example.com') do |ftp|
ftp.login('user', 'pass')
ftp.getbinaryfile('large_file.zip', 'local_copy.zip')
end
Basic Usage
Establishing FTP connections requires creating a Net::FTP instance and calling connection methods. The constructor accepts hostname, username, and password parameters, or connections can be made manually using separate method calls.
require 'net/ftp'
# Direct connection with credentials
ftp = Net::FTP.new('ftp.example.com', username: 'user', password: 'secret')
# Manual connection steps
ftp = Net::FTP.new
ftp.connect('ftp.example.com', 21)
ftp.login('user', 'secret')
File downloads use getbinaryfile
for binary data and gettextfile
for text content. Binary mode preserves exact byte sequences while text mode may perform line ending conversions depending on the server platform.
# Download binary file
ftp.getbinaryfile('archive.tar.gz', '/local/path/archive.tar.gz')
# Download text file with potential line ending conversion
ftp.gettextfile('readme.txt', '/local/readme.txt')
# Download to current directory using remote filename
ftp.getbinaryfile('document.pdf')
File uploads follow similar patterns with putbinaryfile
and puttextfile
methods. The local file path comes first, followed by the optional remote path. Without specifying a remote path, Ruby uses the local filename.
# Upload binary file
ftp.putbinaryfile('/local/image.jpg', 'uploads/image.jpg')
# Upload text file
ftp.puttextfile('/local/config.txt', 'config/settings.txt')
# Upload using same filename
ftp.putbinaryfile('/local/data.csv')
Directory navigation uses standard Unix-style commands. The chdir
method changes the current working directory, while pwd
returns the current path. Directory listings use list
for detailed information or nlst
for filename-only output.
# Navigate directories
ftp.chdir('/pub/software')
current_dir = ftp.pwd # => "/pub/software"
# List directory contents
detailed_list = ftp.list # Includes permissions, size, dates
filenames_only = ftp.nlst # Array of filenames
# Create and remove directories
ftp.mkdir('new_folder')
ftp.rmdir('old_folder')
Advanced Usage
Transfer mode configuration affects how Net::FTP handles data connections and file content. The passive
property controls whether the client initiates data connections (passive mode) or waits for server connections (active mode). Passive mode works better with firewalls and NAT configurations.
# Configure passive mode (default is typically true)
ftp.passive = true
# Active mode for servers that require it
ftp.passive = false
# Check current mode
puts "Using passive mode: #{ftp.passive}"
Binary versus ASCII transfer modes determine how file content is processed during transfer. Binary mode transfers exact byte sequences without modification, while ASCII mode may convert line endings between systems.
# Set transfer mode explicitly
ftp.binary = true # Binary mode for executables, images, archives
ftp.binary = false # ASCII mode for text files
# Transfer mode affects subsequent operations
ftp.binary = true
ftp.getbinaryfile('program.exe') # Preserves exact bytes
ftp.binary = false
ftp.gettextfile('script.sh') # May convert line endings
Progress monitoring during large file transfers uses block parameters that receive byte counts. This enables progress bars, transfer rate calculations, and user feedback during long operations.
# Monitor download progress
ftp.getbinaryfile('large_file.iso', 'local_copy.iso') do |data|
transferred_bytes = data.length
print "." # Simple progress indicator
end
# More sophisticated progress tracking
total_size = ftp.size('large_file.iso')
transferred = 0
ftp.getbinaryfile('large_file.iso', 'local_copy.iso') do |data|
transferred += data.length
percent = (transferred.to_f / total_size * 100).round(2)
puts "Progress: #{percent}% (#{transferred}/#{total_size} bytes)"
end
Directory operations support recursive patterns and detailed file information retrieval. The list
method accepts directory arguments and glob patterns, while custom parsing handles various server response formats.
# List specific directory without changing current directory
files = ftp.list('/pub/software')
# Parse detailed listing information
ftp.list.each do |entry|
# Parse standard Unix ls -l format
parts = entry.split
permissions = parts[0]
size = parts[4]
filename = parts[8..-1].join(' ')
puts "#{filename}: #{size} bytes, permissions: #{permissions}"
end
# Get file metadata without full listing
file_size = ftp.size('important_file.dat')
mod_time = ftp.mtime('config.xml')
Multiple file operations require careful connection management and error handling. Batch operations benefit from maintaining single connections rather than reconnecting for each file.
# Efficient batch download
files_to_download = ['file1.txt', 'file2.dat', 'file3.log']
Net::FTP.open('ftp.example.com') do |ftp|
ftp.login('user', 'password')
ftp.chdir('/downloads')
files_to_download.each do |filename|
begin
ftp.getbinaryfile(filename, "local/#{filename}")
puts "Downloaded: #{filename}"
rescue Net::FTPPermError => e
puts "Permission denied for #{filename}: #{e.message}"
rescue Net::FTPTempError => e
puts "Temporary error downloading #{filename}: #{e.message}"
end
end
end
Error Handling & Debugging
Net::FTP defines a comprehensive exception hierarchy for different types of FTP errors. Understanding these exceptions enables proper error handling and recovery strategies in production applications.
begin
ftp = Net::FTP.new('ftp.example.com')
ftp.login('user', 'wrong_password')
rescue Net::FTPPermError => e
puts "Authentication failed: #{e.message}"
# Handle permission/authentication errors
rescue Net::FTPTempError => e
puts "Temporary server error: #{e.message}"
# Implement retry logic for temporary failures
rescue Net::FTPProtoError => e
puts "Protocol error: #{e.message}"
# Handle malformed responses or protocol violations
rescue Net::FTPError => e
puts "General FTP error: #{e.message}"
# Catch-all for other FTP-related errors
end
Connection timeout and network issues require specific handling patterns. Ruby's standard timeout mechanisms work with Net::FTP operations, but require careful application to avoid leaving connections in inconsistent states.
require 'timeout'
def robust_ftp_operation(hostname, username, password)
ftp = nil
begin
Timeout::timeout(30) do # 30 second connection timeout
ftp = Net::FTP.new(hostname)
ftp.login(username, password)
end
# Perform operations with shorter timeouts
Timeout::timeout(300) do # 5 minutes for file operations
yield ftp
end
rescue Timeout::Error => e
puts "Operation timed out: #{e.message}"
raise
rescue Net::FTPError => e
puts "FTP error: #{e.message}"
raise
ensure
ftp&.close rescue nil # Ensure connection cleanup
end
end
# Usage with timeout protection
robust_ftp_operation('ftp.example.com', 'user', 'pass') do |ftp|
ftp.getbinaryfile('large_file.zip')
end
Response code inspection provides deeper insight into server behavior and error conditions. Net::FTP exposes the last_response
method to access raw FTP server responses.
ftp = Net::FTP.new('ftp.example.com')
ftp.login('user', 'password')
begin
ftp.chdir('/restricted_directory')
rescue Net::FTPPermError => e
response = ftp.last_response
puts "Server response: #{response}"
# Parse response code for specific handling
case response.split(' ').first
when '550'
puts "Directory not found or permission denied"
when '553'
puts "Filename not allowed"
else
puts "Unknown permission error"
end
end
Debugging connection issues often requires examining the FTP protocol conversation. Setting debug output provides visibility into command-response exchanges between client and server.
# Enable debug output
ftp = Net::FTP.new
ftp.debug_mode = true
# All FTP commands and responses will be printed
ftp.connect('ftp.example.com')
ftp.login('user', 'password')
# Output shows:
# -> "USER user"
# <- "331 Password required for user"
# -> "PASS password"
# <- "230 User logged in, proceed"
Recovery strategies for partial transfers and interrupted operations depend on server capabilities and file characteristics. Some servers support restart commands for resuming interrupted transfers.
def resume_download(ftp, remote_file, local_file)
if File.exist?(local_file)
# Check if server supports restart
begin
ftp.rest(File.size(local_file)) # Resume from existing file size
File.open(local_file, 'ab') do |file| # Append mode
ftp.retrbinary("RETR #{remote_file}") do |data|
file.write(data)
end
end
rescue Net::FTPProtoError
puts "Server doesn't support resume, starting fresh download"
File.delete(local_file)
ftp.getbinaryfile(remote_file, local_file)
end
else
ftp.getbinaryfile(remote_file, local_file)
end
end
Production Patterns
Production FTP operations require robust connection pooling and retry mechanisms to handle network instability and server load issues. Implementing connection pools prevents resource exhaustion while providing consistent performance.
class FTPConnectionPool
def initialize(host, username, password, pool_size = 5)
@host = host
@username = username
@password = password
@pool = []
@pool_mutex = Mutex.new
pool_size.times { @pool << create_connection }
end
def with_connection
connection = @pool_mutex.synchronize { @pool.pop }
if connection.nil?
connection = create_connection
end
begin
yield connection
ensure
@pool_mutex.synchronize { @pool.push(connection) }
end
end
private
def create_connection
ftp = Net::FTP.new(@host)
ftp.login(@username, @password)
ftp.passive = true
ftp
rescue Net::FTPError => e
puts "Failed to create FTP connection: #{e.message}"
nil
end
end
# Usage in production
ftp_pool = FTPConnectionPool.new('ftp.example.com', 'user', 'password')
# Thread-safe operations
threads = []
files_to_process = ['file1.dat', 'file2.dat', 'file3.dat']
files_to_process.each do |filename|
threads << Thread.new do
ftp_pool.with_connection do |ftp|
ftp.getbinaryfile(filename, "processed/#{filename}")
end
end
end
threads.each(&:join)
Monitoring and logging FTP operations in production environments requires structured logging and metrics collection. Integrating with application monitoring systems provides visibility into transfer performance and failure patterns.
class ProductionFTPClient
attr_reader :logger, :metrics
def initialize(host, username, password, logger: Logger.new(STDOUT))
@host = host
@username = username
@password = password
@logger = logger
@metrics = {}
end
def download_file(remote_path, local_path)
start_time = Time.now
file_size = 0
logger.info("Starting download: #{remote_path} -> #{local_path}")
Net::FTP.open(@host) do |ftp|
ftp.login(@username, @password)
ftp.passive = true
file_size = ftp.size(remote_path)
logger.info("File size: #{file_size} bytes")
ftp.getbinaryfile(remote_path, local_path) do |data|
# Progress callback for monitoring
transferred = File.size(local_path) rescue 0
progress = (transferred.to_f / file_size * 100).round(2)
if transferred % (1024 * 1024) == 0 # Log every MB
logger.debug("Download progress: #{progress}%")
end
end
end
duration = Time.now - start_time
transfer_rate = file_size / duration / 1024 # KB/s
logger.info("Download completed in #{duration.round(2)}s at #{transfer_rate.round(2)} KB/s")
# Store metrics for monitoring
@metrics[:last_transfer] = {
file: remote_path,
size: file_size,
duration: duration,
rate: transfer_rate,
timestamp: Time.now
}
rescue Net::FTPError => e
logger.error("FTP download failed: #{e.message}")
logger.error("Remote: #{remote_path}, Local: #{local_path}")
raise
end
end
Configuration management for production FTP clients should externalize connection parameters and provide environment-specific settings. Using configuration objects enables testing and deployment flexibility.
# config/ftp_config.yml
production:
host: "ftp.production.com"
username: "prod_user"
password: "secure_password"
passive_mode: true
timeout: 300
retry_attempts: 3
retry_delay: 5
development:
host: "ftp.dev.com"
username: "dev_user"
password: "dev_password"
passive_mode: true
timeout: 60
retry_attempts: 1
retry_delay: 1
# FTP client with configuration
class ConfigurableFTPClient
def initialize(env = 'production')
config_file = File.read("config/ftp_config.yml")
@config = YAML.safe_load(config_file)[env]
@logger = Logger.new(STDOUT)
end
def execute_with_retry(operation)
attempts = 0
begin
attempts += 1
Net::FTP.open(@config['host']) do |ftp|
ftp.login(@config['username'], @config['password'])
ftp.passive = @config['passive_mode']
Timeout::timeout(@config['timeout']) do
yield ftp
end
end
rescue Net::FTPError, Timeout::Error => e
if attempts < @config['retry_attempts']
@logger.warn("FTP operation failed (attempt #{attempts}): #{e.message}")
sleep(@config['retry_delay'])
retry
else
@logger.error("FTP operation failed after #{attempts} attempts: #{e.message}")
raise
end
end
end
end
Health check implementations for FTP services require lightweight connection tests that verify server availability without impacting performance. Regular health checks enable proactive monitoring and alerting.
class FTPHealthChecker
def initialize(host, username, password)
@host = host
@username = username
@password = password
@last_check = nil
@status = :unknown
end
def healthy?
return @status == :healthy if recent_check?
perform_health_check
@status == :healthy
end
def health_status
{
status: @status,
last_check: @last_check,
host: @host,
message: @last_error&.message
}
end
private
def recent_check?
@last_check && (Time.now - @last_check) < 60 # 1 minute cache
end
def perform_health_check
@last_check = Time.now
begin
Timeout::timeout(10) do
Net::FTP.open(@host) do |ftp|
ftp.login(@username, @password)
ftp.pwd # Simple operation to verify functionality
end
end
@status = :healthy
@last_error = nil
rescue Net::FTPError, Timeout::Error => e
@status = :unhealthy
@last_error = e
end
end
end
Common Pitfalls
Connection leaks represent the most frequent issue in FTP applications. Failing to close connections properly exhausts server connection limits and causes application failures. Always use block-based resource management or explicit cleanup in ensure blocks.
# WRONG: Connection leak
def bad_ftp_download(host, user, pass, remote_file)
ftp = Net::FTP.new(host)
ftp.login(user, pass)
ftp.getbinaryfile(remote_file)
# Missing ftp.close - connection remains open
end
# CORRECT: Automatic cleanup
def good_ftp_download(host, user, pass, remote_file)
Net::FTP.open(host) do |ftp|
ftp.login(user, pass)
ftp.getbinaryfile(remote_file)
end # Connection automatically closed
end
# CORRECT: Manual cleanup with error handling
def manual_ftp_download(host, user, pass, remote_file)
ftp = Net::FTP.new(host)
begin
ftp.login(user, pass)
ftp.getbinaryfile(remote_file)
ensure
ftp.close if ftp
end
end
Transfer mode confusion causes file corruption when binary files are transferred in ASCII mode. Always verify transfer mode settings before file operations, especially when handling mixed content types.
# WRONG: Binary file corrupted by ASCII transfer
ftp = Net::FTP.new('ftp.example.com')
ftp.login('user', 'password')
# Default mode varies by server - don't assume
ftp.gettextfile('program.exe') # Corrupts binary executable
# CORRECT: Explicit binary mode for binary files
ftp = Net::FTP.new('ftp.example.com')
ftp.login('user', 'password')
ftp.binary = true # Explicit binary mode
ftp.getbinaryfile('program.exe')
# CORRECT: Mode verification before operations
def transfer_file(ftp, filename, is_binary = true)
if is_binary
ftp.binary = true
ftp.getbinaryfile(filename)
else
ftp.binary = false
ftp.gettextfile(filename)
end
end
Passive mode misconfigurations cause connection failures in firewall and NAT environments. Most modern networks require passive mode, but some legacy servers only support active mode. Always test both modes during development.
# Test connection modes systematically
def test_connection_modes(host, user, pass)
modes = [true, false] # passive and active
modes.each do |passive_mode|
begin
Net::FTP.open(host) do |ftp|
ftp.passive = passive_mode
ftp.login(user, pass)
ftp.list # Test data connection
puts "#{passive_mode ? 'Passive' : 'Active'} mode: SUCCESS"
return passive_mode
end
rescue Net::FTPError => e
puts "#{passive_mode ? 'Passive' : 'Active'} mode: FAILED - #{e.message}"
end
end
raise "Both connection modes failed"
end
Directory path assumptions fail when servers use different path separators or starting directories. Always use absolute paths or verify current directory before relative path operations.
# WRONG: Assumes Unix-style paths and root directory
ftp.chdir('/pub/files') # May fail on Windows FTP servers
ftp.getbinaryfile('../sensitive.txt') # Directory traversal attempt
# CORRECT: Verify current directory and use safe paths
current_dir = ftp.pwd
puts "Current directory: #{current_dir}"
# Build paths safely
target_dir = File.join(current_dir, 'pub', 'files')
ftp.chdir(target_dir)
# Validate paths before use
def safe_chdir(ftp, path)
# Remove directory traversal attempts
clean_path = path.gsub(/\.\.\//, '')
begin
ftp.chdir(clean_path)
rescue Net::FTPPermError => e
puts "Directory access denied: #{clean_path}"
raise
end
end
Exception handling gaps allow errors to propagate without proper cleanup or user feedback. FTP operations have numerous failure modes that require specific handling strategies.
# WRONG: Catches all exceptions broadly
begin
ftp_operations
rescue => e
puts "Something went wrong"
end
# CORRECT: Specific exception handling with appropriate responses
begin
Net::FTP.open('ftp.example.com') do |ftp|
ftp.login('user', 'password')
ftp.getbinaryfile('important_file.dat')
end
rescue Net::FTPPermError => e
# Authentication or permission issues
puts "Access denied: Check credentials and permissions"
log_security_event(e)
rescue Net::FTPTempError => e
# Temporary server issues - retry appropriate
puts "Server temporarily unavailable: #{e.message}"
schedule_retry
rescue Net::FTPProtoError => e
# Protocol violations - usually permanent
puts "Protocol error - server may be incompatible: #{e.message}"
alert_administrators(e)
rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT => e
# Network connectivity issues
puts "Cannot reach FTP server: #{e.message}"
check_network_connectivity
rescue => e
# Unexpected errors
puts "Unexpected error during FTP operation: #{e.message}"
report_bug(e)
raise # Re-raise unexpected errors
end
File size and disk space issues cause partial transfers and application crashes. Always verify available disk space before large downloads and monitor transfer progress to detect incomplete operations.
def safe_download(ftp, remote_file, local_path)
# Check remote file size
remote_size = ftp.size(remote_file)
puts "Remote file size: #{remote_size} bytes"
# Check available disk space
stat = File.statvfs(File.dirname(local_path))
available_space = stat.bavail * stat.frsize
if remote_size > available_space
raise "Insufficient disk space: need #{remote_size}, have #{available_space}"
end
# Monitor transfer progress
downloaded = 0
ftp.getbinaryfile(remote_file, local_path) do |data|
downloaded += data.length
end
# Verify complete transfer
local_size = File.size(local_path)
if local_size != remote_size
File.delete(local_path) # Remove incomplete file
raise "Transfer incomplete: expected #{remote_size}, got #{local_size}"
end
puts "Download verified: #{local_size} bytes"
end
Reference
Core Class Methods
Method | Parameters | Returns | Description |
---|---|---|---|
Net::FTP.new(host = nil, **options) |
host (String), options (Hash) |
Net::FTP |
Creates new FTP client instance |
Net::FTP.open(host, **options) |
host (String), options (Hash) |
Net::FTP |
Creates and yields FTP client with automatic cleanup |
#connect(host, port = 21) |
host (String), port (Integer) |
self |
Establishes connection to FTP server |
#login(user = 'anonymous', passwd = nil, acct = nil) |
user (String), passwd (String), acct (String) |
String |
Authenticates with FTP server |
#close |
None | nil |
Closes FTP connection |
#quit |
None | String |
Sends QUIT command and closes connection |
File Transfer Methods
Method | Parameters | Returns | Description |
---|---|---|---|
#getbinaryfile(remotefile, localfile = File.basename(remotefile), blocksize = DEFAULT_BLOCKSIZE) |
remotefile (String), localfile (String), blocksize (Integer) |
nil |
Downloads file in binary mode |
#gettextfile(remotefile, localfile = File.basename(remotefile)) |
remotefile (String), localfile (String) |
nil |
Downloads file in ASCII mode |
#putbinaryfile(localfile, remotefile = File.basename(localfile), blocksize = DEFAULT_BLOCKSIZE) |
localfile (String), remotefile (String), blocksize (Integer) |
nil |
Uploads file in binary mode |
#puttextfile(localfile, remotefile = File.basename(localfile)) |
localfile (String), remotefile (String) |
nil |
Uploads file in ASCII mode |
#retrbinary(cmd, blocksize = DEFAULT_BLOCKSIZE) |
cmd (String), blocksize (Integer) |
nil |
Retrieves file in binary mode with block |
#retrlines(cmd) |
cmd (String) |
nil |
Retrieves file in ASCII mode with block |
#storbinary(cmd, file, blocksize = DEFAULT_BLOCKSIZE) |
cmd (String), file (IO), blocksize (Integer) |
nil |
Stores file in binary mode |
#storlines(cmd, file) |
cmd (String), file (IO) |
nil |
Stores file in ASCII mode |
Directory Operations
Method | Parameters | Returns | Description |
---|---|---|---|
#pwd |
None | String |
Returns current working directory |
#chdir(dirname) |
dirname (String) |
String |
Changes current directory |
#list(dir = nil) |
dir (String) |
Array<String> |
Returns detailed directory listing |
#nlst(dir = nil) |
dir (String) |
Array<String> |
Returns directory filenames only |
#mkdir(dirname) |
dirname (String) |
String |
Creates directory |
#rmdir(dirname) |
dirname (String) |
String |
Removes empty directory |
#delete(filename) |
filename (String) |
String |
Deletes file |
#rename(fromname, toname) |
fromname (String), toname (String) |
String |
Renames file or directory |
File Information Methods
Method | Parameters | Returns | Description |
---|---|---|---|
#size(filename) |
filename (String) |
Integer |
Returns file size in bytes |
#mtime(filename, local = false) |
filename (String), local (Boolean) |
Time |
Returns file modification time |
#stat(filename) |
filename (String) |
String |
Returns file status information |
#system(cmd) |
cmd (String) |
String |
Executes system command on server |
Configuration Properties
Property | Type | Default | Description |
---|---|---|---|
#passive |
Boolean |
true |
Controls passive/active data connection mode |
#binary |
Boolean |
true |
Controls binary/ASCII transfer mode |
#debug_mode |
Boolean |
false |
Enables FTP protocol debug output |
#resume |
Boolean |
false |
Enables transfer resume support |
#last_response |
String |
nil |
Last server response message |
#last_response_code |
String |
nil |
Last server response code |
#welcome |
String |
nil |
Server welcome message |
Exception Hierarchy
Exception | Description | Usage |
---|---|---|
Net::FTPError |
Base class for all FTP errors | Catch-all FTP exception handler |
Net::FTPReplyError |
Server returned error response | Invalid commands or server errors |
Net::FTPTempError |
Temporary server error (4xx codes) | Retry-able errors, server busy |
Net::FTPPermError |
Permanent server error (5xx codes) | Authentication, permission, file not found |
Net::FTPProtoError |
Protocol error or unexpected response | Server compatibility issues |
Constructor Options
Option | Type | Description |
---|---|---|
:username |
String |
Username for automatic login |
:password |
String |
Password for automatic login |
:account |
String |
Account string for login |
:passive |
Boolean |
Initial passive mode setting |
:debug_mode |
Boolean |
Enable debug output |
:ssl |
Hash/Boolean |
SSL/TLS configuration |
Common Response Codes
Code | Category | Description |
---|---|---|
150 |
Positive Preliminary | File status okay, opening data connection |
200 |
Positive Completion | Command successful |
226 |
Positive Completion | Transfer complete, closing data connection |
230 |
Positive Completion | User logged in |
331 |
Positive Intermediate | Username okay, password required |
421 |
Negative Transient | Service not available, closing connection |
450 |
Negative Transient | File unavailable, file busy |
530 |
Negative Permanent | Not logged in |
550 |
Negative Permanent | File unavailable, not found |