CrackedRuby logo

CrackedRuby

Net::FTP

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