Overview
Ruby's File class exposes file system metadata through two primary mechanisms: the File.stat
method that returns detailed file statistics, and predicate methods that perform specific file tests. File.stat returns a File::Stat
object containing comprehensive file information including size, timestamps, permissions, and file type. File testing methods like File.exist?
, File.directory?
, and File.readable?
provide boolean responses for specific file characteristics.
The File::Stat
object wraps the underlying operating system's stat structure, providing cross-platform access to file metadata. Ruby normalizes differences between Unix-like systems and Windows, presenting a consistent interface while preserving platform-specific details where necessary.
stat = File.stat('/etc/passwd')
stat.size
# => 2847
stat.mtime
# => 2024-01-15 10:30:22 +0000
File.exist?('/etc/passwd')
# => true
File testing operations perform single system calls, making them efficient for existence checks and basic file property validation. The predicate methods return boolean values, making them suitable for conditional logic and validation workflows.
if File.readable?('/var/log/app.log') && File.size?('/var/log/app.log')
log_content = File.read('/var/log/app.log')
end
Both stat operations and file tests follow symbolic links by default. Ruby provides separate methods with lstat
and l
-prefixed predicates for examining symbolic links themselves rather than their targets.
Basic Usage
File.stat retrieves comprehensive file metadata in a single system call. The returned File::Stat
object provides methods for accessing all standard file attributes available from the operating system's stat structure.
stat = File.stat('application.log')
# File size in bytes
stat.size
# => 1048576
# Last modification time
stat.mtime
# => 2024-01-20 14:35:28 +0000
# File permissions as octal
stat.mode.to_s(8)
# => "100644"
# File type checks
stat.file?
# => true
stat.directory?
# => false
The File::Stat
object caches all file metadata from the single stat system call, allowing multiple attribute accesses without additional file system operations. This makes stat objects efficient for retrieving multiple file properties.
File testing methods provide boolean responses for specific file characteristics. These methods perform targeted checks and return immediately with true or false values.
# Basic existence and type tests
File.exist?('config.yml') # File exists
File.file?('config.yml') # Regular file
File.directory?('logs') # Directory
File.symlink?('current') # Symbolic link
# Permission tests
File.readable?('secret.key') # Read permission
File.writable?('temp.log') # Write permission
File.executable?('script.rb') # Execute permission
# Size and emptiness tests
File.size?('data.csv') # File size or nil if empty/missing
File.zero?('empty.txt') # File exists and has zero size
Combining multiple file tests creates validation logic for file processing workflows. Ruby evaluates boolean operations efficiently, short-circuiting when possible.
def process_config_file(path)
return unless File.exist?(path) && File.readable?(path)
return unless File.file?(path) && File.size?(path)
config = YAML.load_file(path)
validate_config(config)
end
File.lstat and lstat-based predicates examine symbolic links themselves rather than following them to their targets. This distinction matters when managing symbolic link structures or detecting broken links.
# Following symbolic links (default behavior)
File.stat('current') # Stats the target file
File.exist?('current') # True if target exists
# Examining symbolic links directly
File.lstat('current') # Stats the link itself
File.lstat('current').symlink? # True for symbolic links
Time-based file operations frequently use stat information for cache validation, dependency tracking, and cleanup procedures.
def file_newer_than?(file1, file2)
return false unless File.exist?(file1) && File.exist?(file2)
File.stat(file1).mtime > File.stat(file2).mtime
end
def cleanup_old_files(directory, max_age_days)
cutoff_time = Time.now - (max_age_days * 24 * 3600)
Dir.glob(File.join(directory, '*')).each do |file|
next unless File.file?(file)
if File.stat(file).mtime < cutoff_time
File.delete(file)
end
end
end
Error Handling & Debugging
File stat and testing operations raise Errno
exceptions when encountering file system errors. The most common exceptions stem from missing files, permission restrictions, and invalid path specifications. Ruby maps operating system error codes to specific exception classes within the Errno
module.
Errno::ENOENT
occurs when attempting to stat non-existent files or directories. This exception propagates from File.stat
but not from predicate methods, which return false for missing files.
begin
stat = File.stat('missing_file.txt')
rescue Errno::ENOENT => e
puts "File not found: #{e.message}"
# File not found: No such file or directory @ rb_file_s_stat - missing_file.txt
end
# Predicate methods handle missing files gracefully
File.exist?('missing_file.txt')
# => false
File.readable?('missing_file.txt')
# => false
Permission errors manifest as Errno::EACCES
when attempting to stat files without appropriate access rights. This commonly occurs with system files, other users' private directories, or when running processes with restricted privileges.
begin
stat = File.stat('/root/.ssh/id_rsa')
rescue Errno::EACCES => e
puts "Permission denied: #{e.message}"
end
# Safe permission checking
def safely_check_permissions(path)
{
readable: File.readable?(path),
writable: File.writable?(path),
executable: File.executable?(path)
}
rescue Errno::EACCES
{ readable: false, writable: false, executable: false }
end
Path length limitations trigger Errno::ENAMETOOLONG
on some file systems. Unix systems typically limit individual path components to 255 bytes and total paths to 4096 bytes, while Windows has different restrictions.
def validate_path_length(path)
File.stat(path)
true
rescue Errno::ENAMETOOLONG
false
rescue Errno::ENOENT
false # Missing is different from invalid
end
Symbolic link loops cause Errno::ELOOP
when the kernel detects circular references during path resolution. This error occurs when following symbolic links that reference each other or themselves.
# Create circular symbolic link for demonstration
File.symlink('link_b', 'link_a')
File.symlink('link_a', 'link_b')
begin
File.stat('link_a') # Follows symbolic links
rescue Errno::ELOOP => e
puts "Circular symbolic link detected"
end
# lstat avoids following links
File.lstat('link_a').symlink?
# => true
File system race conditions occur when files are deleted or modified between existence checks and subsequent operations. Defensive programming patterns handle these scenarios through exception handling rather than existence checks.
# Race condition prone
if File.exist?(temp_file)
content = File.read(temp_file) # File might be deleted here
end
# Race condition resistant
def safe_read_file(path)
File.read(path)
rescue Errno::ENOENT
nil
rescue Errno::EACCES
warn "Cannot read #{path}: permission denied"
nil
end
Debugging file stat issues requires examining the specific exception details, file system permissions, and path resolution. Ruby provides detailed error messages including the failing operation and file path.
def debug_file_access(path)
puts "Debugging file access for: #{path}"
# Basic existence
puts "exists: #{File.exist?(path)}"
puts "file: #{File.file?(path)}"
puts "directory: #{File.directory?(path)}"
puts "symlink: #{File.symlink?(path)}"
# Permission checks
puts "readable: #{File.readable?(path)}"
puts "writable: #{File.writable?(path)}"
puts "executable: #{File.executable?(path)}"
# Detailed stat information
begin
stat = File.stat(path)
puts "size: #{stat.size}"
puts "mode: #{stat.mode.to_s(8)}"
puts "uid: #{stat.uid}"
puts "gid: #{stat.gid}"
rescue => e
puts "stat failed: #{e.class} - #{e.message}"
end
end
Production Patterns
Production applications rely heavily on file testing for configuration validation, log management, and deployment verification. File stat operations provide the foundation for caching strategies, dependency tracking, and resource monitoring in web applications and background services.
Configuration file validation represents a critical production use case where file testing ensures application startup safety. Applications should verify configuration file accessibility before attempting to parse potentially sensitive data.
class ConfigurationLoader
def initialize(config_path)
@config_path = config_path
validate_config_file!
end
def load
return @cached_config if config_unchanged?
@cached_config = parse_config_file
@last_mtime = File.stat(@config_path).mtime
@cached_config
end
private
def validate_config_file!
raise ConfigError, "Config file missing: #{@config_path}" unless File.exist?(@config_path)
raise ConfigError, "Config file unreadable: #{@config_path}" unless File.readable?(@config_path)
raise ConfigError, "Config file empty: #{@config_path}" if File.zero?(@config_path)
end
def config_unchanged?
@last_mtime && File.stat(@config_path).mtime <= @last_mtime
rescue Errno::ENOENT
false
end
def parse_config_file
YAML.load_file(@config_path)
rescue => e
raise ConfigError, "Invalid config file: #{e.message}"
end
end
Log file monitoring and rotation depends on file stat information to manage disk space and maintain application performance. Production systems implement log rotation based on file size and age thresholds.
class LogRotator
MAX_SIZE = 50 * 1024 * 1024 # 50MB
MAX_AGE = 7 * 24 * 3600 # 7 days
def initialize(log_path)
@log_path = log_path
@rotated_pattern = "#{log_path}.%Y%m%d-%H%M%S"
end
def rotate_if_needed
return unless should_rotate?
timestamp = Time.now.strftime("%Y%m%d-%H%M%S")
archived_path = @log_path + ".#{timestamp}"
File.rename(@log_path, archived_path)
cleanup_old_archives
end
private
def should_rotate?
return false unless File.exist?(@log_path)
stat = File.stat(@log_path)
stat.size > MAX_SIZE || (Time.now - stat.mtime) > MAX_AGE
end
def cleanup_old_archives
pattern = @log_path + ".*"
cutoff = Time.now - (30 * 24 * 3600) # 30 days
Dir.glob(pattern).each do |archived_file|
next unless File.file?(archived_file)
if File.stat(archived_file).mtime < cutoff
File.delete(archived_file)
end
end
end
end
Deployment verification uses file testing to validate application assets and ensure proper file permissions before serving traffic. This prevents runtime errors from missing or inaccessible files.
class DeploymentValidator
REQUIRED_FILES = [
'config/application.yml',
'config/database.yml',
'config/secrets.yml'
].freeze
REQUIRED_DIRECTORIES = [
'tmp/pids',
'tmp/cache',
'log',
'public/assets'
].freeze
def validate!
errors = []
errors.concat(validate_files)
errors.concat(validate_directories)
errors.concat(validate_permissions)
raise DeploymentError, errors.join("\n") if errors.any?
end
private
def validate_files
REQUIRED_FILES.reject { |file| File.exist?(file) && File.readable?(file) }
.map { |file| "Missing or unreadable file: #{file}" }
end
def validate_directories
REQUIRED_DIRECTORIES.reject { |dir| File.directory?(dir) && File.writable?(dir) }
.map { |dir| "Missing or non-writable directory: #{dir}" }
end
def validate_permissions
errors = []
# Check critical file permissions
if File.exist?('config/secrets.yml')
mode = File.stat('config/secrets.yml').mode
if (mode & 0077) != 0 # World or group readable
errors << "Secrets file has insecure permissions"
end
end
errors
end
end
Asset fingerprinting and cache busting rely on file modification times and content hashes derived from file stats. Web applications use this information to generate cache-busting URLs and optimize browser caching.
class AssetFingerprinter
def initialize(assets_path)
@assets_path = assets_path
@fingerprints = {}
end
def fingerprint(asset_name)
asset_path = File.join(@assets_path, asset_name)
# Use cached fingerprint if file unchanged
if @fingerprints[asset_name]
cached_mtime, fingerprint = @fingerprints[asset_name]
current_mtime = File.stat(asset_path).mtime
return fingerprint if current_mtime <= cached_mtime
end
# Generate new fingerprint
content = File.read(asset_path)
fingerprint = Digest::MD5.hexdigest(content)[0, 8]
@fingerprints[asset_name] = [File.stat(asset_path).mtime, fingerprint]
fingerprint
rescue Errno::ENOENT
nil
end
def asset_url(asset_name)
fingerprint = fingerprint(asset_name)
return "/assets/#{asset_name}" unless fingerprint
"/assets/#{asset_name}?v=#{fingerprint}"
end
end
Reference
File.stat Methods
Method | Parameters | Returns | Description |
---|---|---|---|
File.stat(path) |
path (String) |
File::Stat |
Returns file statistics for path |
File.lstat(path) |
path (String) |
File::Stat |
Returns statistics for symlink itself |
File Testing Methods
Method | Parameters | Returns | Description |
---|---|---|---|
File.exist?(path) |
path (String) |
Boolean |
True if file exists |
File.file?(path) |
path (String) |
Boolean |
True if regular file |
File.directory?(path) |
path (String) |
Boolean |
True if directory |
File.symlink?(path) |
path (String) |
Boolean |
True if symbolic link |
File.readable?(path) |
path (String) |
Boolean |
True if readable by effective user |
File.writable?(path) |
path (String) |
Boolean |
True if writable by effective user |
File.executable?(path) |
path (String) |
Boolean |
True if executable by effective user |
File.size?(path) |
path (String) |
Integer or nil |
File size or nil if empty/missing |
File.zero?(path) |
path (String) |
Boolean |
True if file exists and is empty |
File::Stat Instance Methods
Method | Returns | Description |
---|---|---|
#atime |
Time |
Last access time |
#ctime |
Time |
Change time (metadata modification) |
#mtime |
Time |
Last modification time |
#size |
Integer |
File size in bytes |
#mode |
Integer |
File permission bits |
#uid |
Integer |
User ID of file owner |
#gid |
Integer |
Group ID of file |
#dev |
Integer |
Device ID containing file |
#ino |
Integer |
Inode number |
#nlink |
Integer |
Number of hard links |
#blksize |
Integer |
Filesystem block size |
#blocks |
Integer |
Number of blocks allocated |
File::Stat Type Methods
Method | Returns | Description |
---|---|---|
#file? |
Boolean |
True if regular file |
#directory? |
Boolean |
True if directory |
#symlink? |
Boolean |
True if symbolic link |
#pipe? |
Boolean |
True if named pipe (FIFO) |
#socket? |
Boolean |
True if socket |
#chardev? |
Boolean |
True if character device |
#blockdev? |
Boolean |
True if block device |
Common Permission Modes
Octal | Symbolic | Description |
---|---|---|
644 |
-rw-r--r-- |
Owner read/write, group/world read |
755 |
-rwxr-xr-x |
Owner full, group/world read/execute |
600 |
-rw------- |
Owner read/write only |
700 |
-rwx------ |
Owner full access only |
666 |
-rw-rw-rw- |
All read/write |
777 |
-rwxrwxrwx |
All full access |
Exception Classes
Exception | Trigger Condition |
---|---|
Errno::ENOENT |
File or directory does not exist |
Errno::EACCES |
Permission denied |
Errno::ENOTDIR |
Component of path is not a directory |
Errno::ELOOP |
Too many symbolic links |
Errno::ENAMETOOLONG |
Path name too long |
Errno::EIO |
Input/output error |
Symbolic Link Behavior
Method Type | Follows Links | Use Case |
---|---|---|
File.stat |
Yes | Get target file information |
File.lstat |
No | Get link information |
File.exist? |
Yes | Check if target exists |
File.symlink? |
No | Check if path is a link |
Time Comparison Patterns
# File modification comparison
File.stat(file1).mtime > File.stat(file2).mtime
# Age-based filtering
cutoff = Time.now - (days * 24 * 3600)
File.stat(path).mtime < cutoff
# Dependency checking
source_time = File.stat('source.rb').mtime
target_time = File.stat('output.o').mtime rescue Time.at(0)
needs_rebuild = source_time > target_time