Overview
File Transfer Protocol (FTP) and SSH File Transfer Protocol (SFTP) represent two distinct approaches to transferring files over networks. FTP, established in 1971, predates the modern internet and operates as an application-layer protocol built on TCP/IP. SFTP emerged later as part of the SSH-2 protocol suite, providing encrypted file transfer capabilities.
FTP uses a client-server architecture with separate control and data connections. The control connection, typically on port 21, handles commands and responses. The data connection transfers actual file content and directory listings. This dual-connection design creates complexity in firewall configurations and introduces security vulnerabilities, as credentials and data traditionally transmit in plaintext.
SFTP operates differently. Rather than being an extension of FTP, SFTP functions as a subsystem of SSH, using a single encrypted connection on port 22. All commands, responses, and data travel through this secure channel. SFTP provides file transfer, directory listing, file manipulation, and permission management within the encrypted SSH session.
The protocols serve different purposes in modern systems. FTP persists in legacy environments, internal networks with relaxed security requirements, and scenarios where broad client compatibility matters. SFTP dominates in production environments requiring security, compliance, or handling sensitive data. Many organizations maintain both protocols during transition periods, supporting legacy clients while encouraging migration to SFTP.
# FTP connection structure
require 'net/ftp'
ftp = Net::FTP.new
ftp.connect('ftp.example.com')
ftp.login('username', 'password')
# Credentials transmitted in plaintext
# SFTP connection structure
require 'net/sftp'
Net::SFTP.start('sftp.example.com', 'username',
password: 'password') do |sftp|
# All communication encrypted
end
Key Principles
FTP operates through a command-response protocol over two separate TCP connections. The control connection remains open throughout the session, transmitting commands like LIST, RETR (retrieve), STOR (store), and their responses. The data connection opens for each transfer operation, then closes. This separation allows the control connection to remain responsive during large transfers but complicates network administration.
FTP supports two transfer modes: active and passive. Active mode has the server initiate the data connection back to the client, which creates problems with firewalls and NAT. The client sends a PORT command specifying where the server should connect. Passive mode, introduced to address these issues, has the client initiate both connections. The client sends PASV, the server responds with an IP and port, and the client connects to that endpoint for data transfer.
The protocol distinguishes between ASCII and binary transfer modes. ASCII mode converts line endings between systems (CRLF for Windows, LF for Unix), while binary mode transfers bytes unchanged. Using ASCII mode for binary files corrupts data. Modern practice defaults to binary mode unless specifically transferring text files between platforms with different line ending conventions.
SFTP operates as a stateful protocol built atop SSH. After SSH authentication completes, the client requests the SFTP subsystem. The server spawns an SFTP server process attached to the SSH channel. All SFTP messages exchange through this channel as encrypted packets. Each packet contains a message type identifier, request ID for matching responses, and message-specific data.
SFTP messages include SSH_FXP_OPEN for opening files, SSH_FXP_READ and SSH_FXP_WRITE for I/O operations, SSH_FXP_OPENDIR and SSH_FXP_READDIR for directory operations, and SSH_FXP_STAT for file attribute queries. The protocol supports file locking, atomic operations, and attribute manipulation that FTP lacks. SFTP version 3, most widely implemented, provides sufficient functionality for typical use cases. Later versions add features like file name character set support and extended attributes.
Authentication differs fundamentally between protocols. FTP authentication transmits username and password in plaintext across the control connection. FTP supports anonymous access through the username "anonymous" with email addresses as passwords. SFTP authentication uses SSH mechanisms: password authentication, public key authentication, keyboard-interactive authentication, or certificate-based authentication. Public key authentication, where the client proves possession of a private key corresponding to an authorized public key on the server, provides the strongest security without password transmission.
Error handling follows distinct patterns. FTP returns three-digit numeric codes with text messages. Codes in the 200 range indicate success, 300 range requires additional information, 400 range signals temporary errors, and 500 range indicates permanent errors. SFTP uses status codes like SSH_FX_OK, SSH_FX_EOF, SSH_FX_NO_SUCH_FILE, and SSH_FX_PERMISSION_DENIED. Unlike FTP, SFTP status messages accompany most operations rather than only appearing during errors.
Ruby Implementation
Ruby provides protocol support through separate libraries. The standard library includes Net::FTP for FTP operations. SFTP requires the net-sftp gem, which depends on net-ssh for underlying SSH functionality. These libraries expose different APIs reflecting the protocols' architectural differences.
Net::FTP provides instance methods corresponding to FTP commands. Connection establishment requires explicit connect and login calls. The library handles mode switching, connection management, and command encoding.
require 'net/ftp'
ftp = Net::FTP.new
ftp.connect('ftp.example.com', 21)
ftp.login('username', 'password')
# Passive mode (recommended)
ftp.passive = true
# List directory contents
ftp.list.each do |entry|
puts entry
end
# Download file
ftp.getbinaryfile('remote.txt', 'local.txt')
# Upload file
ftp.putbinaryfile('local.txt', 'remote.txt')
# Create directory
ftp.mkdir('new_directory')
# Change directory
ftp.chdir('new_directory')
# Delete file
ftp.delete('obsolete.txt')
ftp.close
Net::FTP provides gettextfile and puttextfile for ASCII mode transfers, but getbinaryfile and putbinaryfile handle most scenarios correctly. The library defaults to binary mode, which prevents line ending corruption.
The retrbinary and storbinary methods provide streaming interfaces accepting blocks for processing data during transfer:
require 'net/ftp'
ftp = Net::FTP.new('ftp.example.com')
ftp.login('user', 'pass')
ftp.passive = true
# Stream download with progress tracking
size = 0
ftp.retrbinary('RETR large_file.zip', 1024) do |chunk|
size += chunk.bytesize
puts "Downloaded #{size} bytes"
# Process chunk without loading entire file in memory
end
# Stream upload from file
File.open('upload.dat', 'rb') do |file|
ftp.storbinary('STOR remote.dat', file, 1024)
end
ftp.close
Net::SFTP operates differently, using a block-based API for connection management. The start method establishes the SSH connection, authenticates, and opens the SFTP channel, yielding an SFTP session object. The connection closes automatically when the block exits.
require 'net/sftp'
Net::SFTP.start('sftp.example.com', 'username',
password: 'password') do |sftp|
# List directory contents with attributes
sftp.dir.foreach('/remote/path') do |entry|
puts "#{entry.name} - #{entry.longname}"
puts "Size: #{entry.attributes.size}"
puts "Permissions: #{entry.attributes.permissions.to_s(8)}"
end
# Download file
sftp.download!('/remote/path/file.txt', 'local.txt')
# Upload file
sftp.upload!('local.txt', '/remote/path/file.txt')
# Create directory
sftp.mkdir!('/remote/path/new_dir')
# Remove file
sftp.remove!('/remote/path/obsolete.txt')
end
Methods ending with exclamation marks (download!, upload!, mkdir!) operate synchronously. Non-bang versions return request objects enabling asynchronous operations:
require 'net/sftp'
Net::SFTP.start('sftp.example.com', 'username',
password: 'password') do |sftp|
# Initiate multiple uploads asynchronously
requests = []
files = ['file1.txt', 'file2.txt', 'file3.txt']
files.each do |filename|
req = sftp.upload("local/#{filename}", "/remote/#{filename}")
requests << req
end
# Wait for all uploads to complete
requests.each { |req| req.wait }
end
Net::SSH provides authentication options including key-based authentication, which eliminates password transmission:
require 'net/sftp'
# Public key authentication
Net::SFTP.start('sftp.example.com', 'username',
keys: ['/home/user/.ssh/id_rsa']) do |sftp|
sftp.upload!('sensitive.dat', '/secure/sensitive.dat')
end
# Key with passphrase
Net::SFTP.start('sftp.example.com', 'username',
keys: ['/home/user/.ssh/id_rsa'],
passphrase: 'key_passphrase') do |sftp|
sftp.download!('/secure/data.json', 'data.json')
end
# Multiple authentication methods
Net::SFTP.start('sftp.example.com', 'username',
password: 'password',
keys: ['/home/user/.ssh/id_rsa'],
auth_methods: ['publickey', 'password']) do |sftp|
# Tries publickey first, falls back to password
end
File streaming prevents memory exhaustion during large transfers:
require 'net/sftp'
Net::SFTP.start('sftp.example.com', 'username',
password: 'password') do |sftp|
# Stream download with progress callback
sftp.download!('/remote/large_file.zip', 'local.zip') do |event, downloader, *args|
case event
when :open
puts "Starting download"
when :get
puts "Downloaded #{args[1]} bytes"
when :finish
puts "Download complete"
end
end
# Stream upload with progress
sftp.upload!('large_local.zip', '/remote/backup.zip') do |event, uploader, *args|
case event
when :open
puts "Starting upload"
when :put
puts "Uploaded #{args[1]} bytes"
when :finish
puts "Upload complete"
end
end
end
Attribute manipulation provides POSIX-style file operations:
require 'net/sftp'
Net::SFTP.start('sftp.example.com', 'username',
password: 'password') do |sftp|
# Get file attributes
attrs = sftp.stat!('/remote/file.txt')
puts "Size: #{attrs.size}"
puts "Modified: #{Time.at(attrs.mtime)}"
puts "Permissions: #{attrs.permissions.to_s(8)}"
# Change permissions
sftp.setstat!('/remote/file.txt', permissions: 0644)
# Check if path exists
begin
sftp.stat!('/remote/maybe_exists.txt')
puts "File exists"
rescue Net::SFTP::StatusException => e
if e.code == 2 # SSH_FX_NO_SUCH_FILE
puts "File does not exist"
else
raise
end
end
# Rename/move file
sftp.rename!('/remote/old_name.txt', '/remote/new_name.txt')
end
Security Implications
FTP security flaws stem from its plaintext design. Credentials transmit unencrypted during authentication. Command and response text travels in clear over the control connection. File contents transfer unencrypted over the data connection. Network observers capture passwords, commands, and data through packet sniffing. FTP offers no mechanism for verifying server identity, enabling man-in-the-middle attacks.
Active mode FTP creates additional security concerns. The server connects back to the client at an IP address the client specifies in the PORT command. Attackers exploit this for port scanning or connection hijacking. The client must accept incoming connections, requiring firewall rules that expose attack surface. NAT configurations break active mode since the client behind NAT cannot specify its real external IP.
Passive mode FTP addresses connection direction issues but maintains plaintext transmission. The server opens a listening port and tells the client where to connect. This requires the server to allow incoming connections on arbitrary high ports, complicating firewall configuration. Organizations often limit passive mode to specific port ranges, then configure firewalls to allow those ranges.
FTPS (FTP Secure) adds TLS/SSL encryption to FTP but remains distinct from SFTP. FTPS operates in explicit mode (starting plaintext then upgrading with AUTH TLS) or implicit mode (encrypted from connection start). FTPS complexity stems from encrypting both control and data connections separately, certificate validation requirements, and continued dual-connection architecture. Firewall traversal remains problematic with FTPS since encrypted control channel prevents inspection of PASV responses containing dynamic port numbers.
require 'net/ftp'
# FTPS explicit mode (starts plain, upgrades with AUTH TLS)
ftp = Net::FTP.new
ftp.connect('ftps.example.com', 21)
ftp.login('username', 'password')
# Note: Standard library FTP class has limited TLS support
# Production FTPS requires third-party gems like double-bag-ftps
SFTP security derives from SSH. All data transfers encrypted using negotiated ciphers. SSH provides server authentication through host keys, preventing impersonation. Multiple authentication methods support password, publickey, or multi-factor approaches. The protocol includes integrity checking detecting tampering or corruption. A single encrypted connection simplifies firewall configuration compared to FTP's dual connections.
Key-based authentication eliminates password transmission entirely. The client proves possession of a private key corresponding to a public key the server trusts. The private key never transmits across the network. An attacker capturing network traffic cannot authenticate even with complete packet capture. Passphrase-protected private keys add defense against key file theft.
require 'net/sftp'
# Generate key pair with ssh-keygen first:
# ssh-keygen -t rsa -b 4096 -f ~/.ssh/deployment_key
# Copy public key to server:
# ssh-copy-id -i ~/.ssh/deployment_key.pub user@server
# Use in Ruby without password
Net::SFTP.start('server.example.com', 'deploy_user',
keys: [File.expand_path('~/.ssh/deployment_key')],
keys_only: true) do |sftp|
# No password sent over network
# Authentication proves key possession
sftp.upload!('application.tar.gz', '/deploy/app.tar.gz')
end
Host key verification prevents man-in-the-middle attacks. SSH clients maintain known_hosts files mapping hostnames to expected host keys. Connection attempts to a host with a different key than recorded trigger warnings. Automated systems should verify host keys explicitly:
require 'net/sftp'
# Strict host key verification
Net::SFTP.start('sftp.example.com', 'username',
password: 'password',
paranoid: true, # Raises if host key unknown
user_known_hosts_file: '/path/to/known_hosts') do |sftp|
# Only connects if host key matches known_hosts
end
# Verify specific host key fingerprint
expected_fingerprint = 'SHA256:abc123...'
Net::SFTP.start('sftp.example.com', 'username',
password: 'password',
paranoid: true,
verify_host_key: lambda { |key|
actual = Digest::SHA256.base64digest(key.to_blob)
actual == expected_fingerprint
}) do |sftp|
# Custom verification logic
end
Credential management impacts both protocols. Hard-coded passwords in source code represent significant vulnerabilities. Environment variables provide basic separation:
require 'net/sftp'
# Load from environment
sftp_host = ENV['SFTP_HOST']
sftp_user = ENV['SFTP_USER']
sftp_password = ENV['SFTP_PASSWORD']
Net::SFTP.start(sftp_host, sftp_user, password: sftp_password) do |sftp|
sftp.upload!('backup.sql', '/backups/backup.sql')
end
Secrets management systems offer better credential security. Vaults store encrypted credentials retrieved at runtime:
require 'net/sftp'
require 'aws-sdk-secretsmanager'
# Retrieve credentials from AWS Secrets Manager
client = Aws::SecretsManager::Client.new(region: 'us-east-1')
secret = client.get_secret_value(secret_id: 'production/sftp')
credentials = JSON.parse(secret.secret_string)
Net::SFTP.start(credentials['host'], credentials['username'],
password: credentials['password']) do |sftp|
sftp.download!('/reports/daily.csv', 'daily.csv')
end
File permission verification prevents unauthorized access on the remote system. SFTP allows querying and setting file permissions:
require 'net/sftp'
Net::SFTP.start('sftp.example.com', 'username',
password: 'password') do |sftp|
# Upload with explicit permissions
sftp.upload!('private.key', '/secure/private.key')
sftp.setstat!('/secure/private.key', permissions: 0600)
# Verify permissions after upload
attrs = sftp.stat!('/secure/private.key')
perms = attrs.permissions & 0777
if perms != 0600
raise "Incorrect permissions: #{perms.to_s(8)}"
end
end
Practical Examples
Batch file downloads require iteration over directory listings. FTP and SFTP provide different mechanisms for retrieving directory contents:
require 'net/sftp'
# Download all CSV files from directory
Net::SFTP.start('sftp.example.com', 'username',
password: 'password') do |sftp|
remote_dir = '/data/exports'
local_dir = './downloads'
# Ensure local directory exists
Dir.mkdir(local_dir) unless Dir.exist?(local_dir)
# Iterate through remote directory
sftp.dir.foreach(remote_dir) do |entry|
next if entry.name == '.' || entry.name == '..'
next unless entry.name.end_with?('.csv')
remote_path = "#{remote_dir}/#{entry.name}"
local_path = File.join(local_dir, entry.name)
puts "Downloading #{entry.name}..."
sftp.download!(remote_path, local_path)
puts "Downloaded #{entry.name} (#{entry.attributes.size} bytes)"
end
end
Recursive directory downloads require depth-first traversal:
require 'net/sftp'
require 'fileutils'
def download_recursive(sftp, remote_path, local_path)
# Create local directory
FileUtils.mkdir_p(local_path)
# Process directory entries
sftp.dir.foreach(remote_path) do |entry|
next if entry.name == '.' || entry.name == '..'
remote_item = "#{remote_path}/#{entry.name}"
local_item = File.join(local_path, entry.name)
if entry.directory?
# Recursively download subdirectory
download_recursive(sftp, remote_item, local_item)
else
# Download file
puts "Downloading #{remote_item}"
sftp.download!(remote_item, local_item)
end
end
end
Net::SFTP.start('sftp.example.com', 'username',
password: 'password') do |sftp|
download_recursive(sftp, '/remote/archive', './local/archive')
end
Automated backup uploads demonstrate scheduled file transfer:
require 'net/sftp'
require 'time'
# Compress and upload database backup
def backup_database(sftp, backup_dir)
timestamp = Time.now.strftime('%Y%m%d_%H%M%S')
backup_file = "backup_#{timestamp}.sql.gz"
# Create compressed backup
system("pg_dump mydb | gzip > #{backup_file}")
# Upload to dated directory
remote_dir = "#{backup_dir}/#{Time.now.strftime('%Y/%m')}"
begin
# Create directory structure if needed
parts = remote_dir.split('/')
current = ''
parts.each do |part|
next if part.empty?
current += "/#{part}"
begin
sftp.mkdir!(current)
rescue Net::SFTP::StatusException => e
# Directory might already exist
raise unless e.code == 4 # SSH_FX_FAILURE (directory exists)
end
end
# Upload backup
remote_path = "#{remote_dir}/#{backup_file}"
puts "Uploading to #{remote_path}"
sftp.upload!(backup_file, remote_path) do |event, uploader, *args|
if event == :put
percent = (args[1].to_f / File.size(backup_file) * 100).round(2)
print "\rProgress: #{percent}%"
end
end
puts "\nUpload complete"
# Verify upload
remote_attrs = sftp.stat!(remote_path)
local_size = File.size(backup_file)
if remote_attrs.size != local_size
raise "Size mismatch: local=#{local_size}, remote=#{remote_attrs.size}"
end
ensure
# Clean up local backup file
File.delete(backup_file) if File.exist?(backup_file)
end
end
Net::SFTP.start('backup.example.com', 'backup_user',
keys: ['/etc/backup/.ssh/id_rsa']) do |sftp|
backup_database(sftp, '/backups/database')
end
Synchronization requires comparing local and remote files to transfer only changes:
require 'net/sftp'
require 'digest'
def sync_directory(sftp, local_dir, remote_dir)
# Build map of remote files
remote_files = {}
sftp.dir.foreach(remote_dir) do |entry|
next if entry.name == '.' || entry.name == '..'
next if entry.directory?
remote_files[entry.name] = {
size: entry.attributes.size,
mtime: entry.attributes.mtime
}
end
# Process local files
Dir.foreach(local_dir) do |filename|
next if filename.start_with?('.')
local_path = File.join(local_dir, filename)
next unless File.file?(local_path)
local_stat = File.stat(local_path)
remote_path = "#{remote_dir}/#{filename}"
should_upload = false
if remote_files[filename]
# File exists remotely, check if local is newer
remote_info = remote_files[filename]
if local_stat.mtime.to_i > remote_info[:mtime] ||
local_stat.size != remote_info[:size]
should_upload = true
puts "Updating #{filename} (modified)"
end
else
# File doesn't exist remotely
should_upload = true
puts "Uploading #{filename} (new)"
end
if should_upload
sftp.upload!(local_path, remote_path)
# Preserve modification time
sftp.setstat!(remote_path, mtime: local_stat.mtime.to_i)
end
end
end
Net::SFTP.start('sftp.example.com', 'username',
password: 'password') do |sftp|
sync_directory(sftp, './local_files', '/remote/files')
end
Log file rotation cleanup removes old files from servers:
require 'net/sftp'
require 'time'
def cleanup_old_logs(sftp, log_dir, days_to_keep)
cutoff_time = Time.now - (days_to_keep * 86400)
deleted_count = 0
freed_bytes = 0
sftp.dir.foreach(log_dir) do |entry|
next if entry.name == '.' || entry.name == '..'
next if entry.directory?
next unless entry.name.end_with?('.log', '.log.gz')
file_time = Time.at(entry.attributes.mtime)
if file_time < cutoff_time
file_path = "#{log_dir}/#{entry.name}"
puts "Deleting #{entry.name} (#{file_time})"
sftp.remove!(file_path)
deleted_count += 1
freed_bytes += entry.attributes.size
end
end
puts "Deleted #{deleted_count} files, freed #{freed_bytes / 1024 / 1024}MB"
end
Net::SFTP.start('log-server.example.com', 'admin',
keys: ['/home/admin/.ssh/id_rsa']) do |sftp|
cleanup_old_logs(sftp, '/var/log/application', 30)
end
Common Pitfalls
Transfer mode confusion causes data corruption. Using ASCII mode for binary files modifies byte sequences that happen to match line ending patterns. Compressed archives, executables, images, and other binary data become corrupted. The corruption often goes unnoticed until file usage fails:
require 'net/ftp'
ftp = Net::FTP.new('ftp.example.com')
ftp.login('user', 'pass')
# WRONG: ASCII mode for binary file
ftp.gettextfile('archive.zip', 'local.zip')
# File corrupted, unzip will fail
# CORRECT: Binary mode for binary file
ftp.getbinaryfile('archive.zip', 'local.zip')
# File intact
ftp.close
Default to binary mode for all transfers unless specifically moving text files between platforms requiring line ending conversion.
Passive mode misconfiguration breaks transfers through firewalls. FTP active mode requires clients to accept incoming connections from arbitrary server IPs. Passive mode solves this by having clients initiate both connections, but servers must allow and advertise passive port ranges:
require 'net/ftp'
ftp = Net::FTP.new
ftp.connect('ftp.example.com')
ftp.login('user', 'pass')
# Passive mode required for firewalled clients
ftp.passive = true
begin
ftp.list
rescue Errno::ETIMEDOUT => e
# Firewall blocked passive data connection
puts "Passive mode timeout - firewall issue"
puts "Server passive port range must be allowed"
end
ftp.close
Server administrators must configure passive port ranges and ensure firewalls permit connections to those ports.
Connection leaks exhaust server resources. Failing to close connections leaves them open consuming server connection slots:
require 'net/ftp'
# WRONG: No ensure block
def download_file(filename)
ftp = Net::FTP.new('ftp.example.com')
ftp.login('user', 'pass')
ftp.passive = true
ftp.getbinaryfile(filename, filename)
ftp.close
# Exception before close leaves connection open
end
# CORRECT: Ensure close
def download_file_safe(filename)
ftp = Net::FTP.new('ftp.example.com')
begin
ftp.login('user', 'pass')
ftp.passive = true
ftp.getbinaryfile(filename, filename)
ensure
ftp.close if ftp
end
end
# BETTER: Block form for SFTP
require 'net/sftp'
Net::SFTP.start('sftp.example.com', 'user', password: 'pass') do |sftp|
sftp.download!(filename, filename)
# Connection closes automatically
end
Known hosts file conflicts prevent SFTP connections. SSH host key verification fails when server keys change:
require 'net/sftp'
begin
Net::SFTP.start('sftp.example.com', 'user',
password: 'pass',
paranoid: true) do |sftp|
sftp.download!('file.txt', 'file.txt')
end
rescue Net::SSH::HostKeyMismatch => e
puts "Host key mismatch"
puts "Server presented: #{e.fingerprint}"
puts "Expected: #{e.expected}"
puts "Remove old key: ssh-keygen -R sftp.example.com"
puts "Or set paranoid: false (insecure)"
end
Setting paranoid: false disables host key verification, eliminating man-in-the-middle protection. Only use for testing, never production.
Path separator confusion occurs when mixing Windows and Unix systems. SFTP uses forward slashes regardless of client platform:
require 'net/sftp'
Net::SFTP.start('sftp.example.com', 'user', password: 'pass') do |sftp|
# WRONG: Windows-style path
# sftp.download!('\\remote\\path\\file.txt', 'local.txt')
# Error: path not found
# CORRECT: Unix-style path (always)
sftp.download!('/remote/path/file.txt', 'local.txt')
# Build paths programmatically
parts = ['remote', 'path', 'file.txt']
remote_path = '/' + parts.join('/')
sftp.download!(remote_path, 'local.txt')
end
Permission errors occur when uploaded files have restrictive permissions. SFTP inherits the server's umask, which might create files without group or world readability:
require 'net/sftp'
Net::SFTP.start('sftp.example.com', 'user', password: 'pass') do |sftp|
# Upload creates file with server's umask
sftp.upload!('public.html', '/var/www/public.html')
# File might have 0600, making it unreadable by web server
# Explicitly set permissions
sftp.setstat!('/var/www/public.html', permissions: 0644)
# Or check and fix if needed
attrs = sftp.stat!('/var/www/public.html')
current_perms = attrs.permissions & 0777
if current_perms != 0644
sftp.setstat!('/var/www/public.html', permissions: 0644)
end
end
Large file memory consumption occurs when loading entire files into memory. Stream transfers for files exceeding available RAM:
require 'net/sftp'
Net::SFTP.start('sftp.example.com', 'user', password: 'pass') do |sftp|
# WRONG: Loads entire file in memory
# data = sftp.file.open('/huge/file.dat') { |f| f.read }
# OOM error on large files
# CORRECT: Stream with block
sftp.download!('/huge/file.dat', 'local.dat') do |event, downloader, *args|
case event
when :get
# Process chunks as they arrive
# args[1] is bytes downloaded so far
end
end
end
Timeout configuration prevents indefinite hangs. Network interruptions or slow servers cause connections to block:
require 'net/sftp'
require 'timeout'
begin
Timeout.timeout(300) do # 5 minute overall timeout
Net::SFTP.start('slow-server.example.com', 'user',
password: 'pass',
timeout: 30) do |sftp| # 30 second operation timeout
sftp.download!('/large/file.zip', 'file.zip')
end
end
rescue Timeout::Error
puts "Transfer timed out"
end
Reference
Protocol Comparison
| Aspect | FTP | SFTP |
|---|---|---|
| Port | 21 (control), 20 (data) or dynamic | 22 (single connection) |
| Encryption | None (plaintext) | All traffic encrypted |
| Authentication | Username/password plaintext | SSH methods (key, password, certificate) |
| Connections | Separate control and data | Single SSH channel |
| Firewall Traversal | Complex (active/passive modes) | Simple (one port) |
| Data Integrity | No built-in verification | SSH integrity checking |
| File Operations | Basic (upload, download, delete) | Extended (chmod, chown, symlinks) |
| Performance | Slightly faster (no encryption overhead) | Minimal overhead with modern CPUs |
| Standard | RFC 959 (1985) | SSH File Transfer Protocol (draft) |
FTP Response Codes
| Code | Meaning | Description |
|---|---|---|
| 150 | File status okay | About to open data connection |
| 200 | Command okay | Generic success |
| 220 | Service ready | Ready for new user |
| 226 | Closing data connection | Transfer complete |
| 230 | User logged in | Authentication successful |
| 250 | Requested file action okay | File operation completed |
| 331 | Username okay, need password | Waiting for password |
| 421 | Service not available | Server shutting down |
| 425 | Cannot open data connection | Data connection failed |
| 426 | Connection closed | Transfer aborted |
| 450 | File unavailable | File busy or locked |
| 500 | Syntax error | Command not recognized |
| 530 | Not logged in | Authentication required |
| 550 | Action not taken | File unavailable, permission denied |
Net::FTP Methods
| Method | Purpose | Example |
|---|---|---|
| connect | Establish control connection | ftp.connect('host', 21) |
| login | Authenticate user | ftp.login('user', 'pass') |
| passive= | Set passive mode | ftp.passive = true |
| chdir | Change remote directory | ftp.chdir('/pub') |
| mkdir | Create directory | ftp.mkdir('newdir') |
| list | List directory (formatted) | ftp.list |
| nlst | List directory (names only) | ftp.nlst |
| getbinaryfile | Download binary file | ftp.getbinaryfile('remote', 'local') |
| putbinaryfile | Upload binary file | ftp.putbinaryfile('local', 'remote') |
| gettextfile | Download text file | ftp.gettextfile('remote.txt', 'local.txt') |
| puttextfile | Upload text file | ftp.puttextfile('local.txt', 'remote.txt') |
| delete | Delete file | ftp.delete('obsolete.txt') |
| rename | Rename file | ftp.rename('old.txt', 'new.txt') |
| retrbinary | Stream download | ftp.retrbinary('RETR file') { chunk } |
| storbinary | Stream upload | ftp.storbinary('STOR file', io) |
| close | Close connection | ftp.close |
Net::SFTP Methods
| Method | Purpose | Example |
|---|---|---|
| start | Connect with block | Net::SFTP.start(host, user) { sftp } |
| download! | Download file synchronously | sftp.download!(remote, local) |
| upload! | Upload file synchronously | sftp.upload!(local, remote) |
| download | Download file asynchronously | req = sftp.download(remote, local) |
| upload | Upload file asynchronously | req = sftp.upload(local, remote) |
| dir.foreach | Iterate directory entries | sftp.dir.foreach(path) { entry } |
| dir.entries | Get directory entries array | entries = sftp.dir.entries(path) |
| mkdir! | Create directory | sftp.mkdir!(path) |
| rmdir! | Remove directory | sftp.rmdir!(path) |
| remove! | Delete file | sftp.remove!(path) |
| rename! | Rename or move file | sftp.rename!(old, new) |
| stat! | Get file attributes | attrs = sftp.stat!(path) |
| lstat! | Get link attributes | attrs = sftp.lstat!(path) |
| setstat! | Set file attributes | sftp.setstat!(path, permissions: 0644) |
| readlink! | Read symbolic link | target = sftp.readlink!(path) |
| symlink! | Create symbolic link | sftp.symlink!(target, link) |
Connection Options
| Option | Protocol | Description | Example |
|---|---|---|---|
| passive | FTP | Use passive mode | ftp.passive = true |
| keys | SFTP | Private key files | keys: ['/path/to/key'] |
| keys_only | SFTP | Disable password auth | keys_only: true |
| password | Both | Password authentication | password: 'secret' |
| timeout | SFTP | Operation timeout seconds | timeout: 30 |
| paranoid | SFTP | Host key verification | paranoid: true |
| user_known_hosts_file | SFTP | Known hosts file path | user_known_hosts_file: '/path' |
| passphrase | SFTP | Private key passphrase | passphrase: 'keypass' |
| auth_methods | SFTP | Allowed auth methods | auth_methods: ['publickey'] |
| compression | SFTP | Enable compression | compression: true |
File Attribute Flags
| Flag | Value | Meaning |
|---|---|---|
| SSH_FILEXFER_ATTR_SIZE | 0x00000001 | File size included |
| SSH_FILEXFER_ATTR_UIDGID | 0x00000002 | User and group ID included |
| SSH_FILEXFER_ATTR_PERMISSIONS | 0x00000004 | Permissions included |
| SSH_FILEXFER_ATTR_ACMODTIME | 0x00000008 | Access and modification time included |
| SSH_FILEXFER_ATTR_EXTENDED | 0x80000000 | Extended attributes included |
SFTP Status Codes
| Code | Name | Meaning |
|---|---|---|
| 0 | SSH_FX_OK | Success |
| 1 | SSH_FX_EOF | End of file |
| 2 | SSH_FX_NO_SUCH_FILE | File does not exist |
| 3 | SSH_FX_PERMISSION_DENIED | Permission denied |
| 4 | SSH_FX_FAILURE | Generic failure |
| 5 | SSH_FX_BAD_MESSAGE | Malformed message |
| 6 | SSH_FX_NO_CONNECTION | No connection to server |
| 7 | SSH_FX_CONNECTION_LOST | Connection lost |
| 8 | SSH_FX_OP_UNSUPPORTED | Operation not supported |
Unix File Permissions
| Octal | Binary | Symbolic | Description |
|---|---|---|---|
| 0400 | r-------- | User read | Owner can read |
| 0200 | -w------- | User write | Owner can write |
| 0100 | --x------ | User execute | Owner can execute |
| 0040 | ---r----- | Group read | Group can read |
| 0020 | ----w---- | Group write | Group can write |
| 0010 | -----x--- | Group execute | Group can execute |
| 0004 | ------r-- | Other read | Others can read |
| 0002 | -------w- | Other write | Others can write |
| 0001 | --------x | Other execute | Others can execute |
Common Permission Combinations
| Octal | Symbolic | Use Case |
|---|---|---|
| 0644 | rw-r--r-- | Regular files readable by all |
| 0755 | rwxr-xr-x | Executable files and directories |
| 0600 | rw------- | Private files (keys, configs) |
| 0700 | rwx------ | Private directories |
| 0666 | rw-rw-rw- | World-writable files (rare) |
| 0777 | rwxrwxrwx | World-writable directories (avoid) |