CrackedRuby logo

CrackedRuby

File.stat and File Testing

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