CrackedRuby logo

CrackedRuby

Pathname Class

Overview

Pathname provides an object-oriented approach to file path manipulation in Ruby. Rather than working with string-based paths directly, Pathname wraps path strings and offers methods for common operations like joining, normalization, and file system queries.

The Pathname class sits between raw string paths and Ruby's File and Dir classes. It handles platform-specific path separators automatically and provides a chainable interface for building complex path operations. When you create a Pathname object, it stores the path internally and delegates actual file system operations to File and Dir when needed.

Ruby's Pathname class addresses several problems with string-based path manipulation. String concatenation for paths can introduce errors with trailing slashes and separator inconsistencies across platforms. Pathname handles these details internally and provides methods that return new Pathname objects, allowing for method chaining.

require 'pathname'

# Creating pathname objects
path = Pathname.new('/usr/local/bin')
relative_path = Pathname.new('config/database.yml')

# Path information
path.absolute?  # => true
path.directory? # => true (if directory exists)
path.basename   # => #<Pathname:bin>

The class integrates with Ruby's File and Dir classes by forwarding method calls when appropriate. Methods like read, write, and size delegate to File, while entries and mkdir delegate to Dir. This design maintains the object-oriented interface while leveraging existing file system functionality.

Pathname objects are immutable. Operations that modify paths return new Pathname instances rather than changing the existing object. This approach prevents accidental path corruption and makes the API safer for concurrent usage.

base = Pathname.new('/app')
config = base.join('config', 'settings.yml')
# base remains unchanged, config is new Pathname object

# Method chaining
log_file = Pathname.new('/var/log')
  .join('application.log')
  .expand_path
# => #<Pathname:/var/log/application.log>

Basic Usage

Creating Pathname objects requires passing a string path to the constructor. The string can represent absolute or relative paths, and Pathname preserves the original format until explicitly converted.

# Different path types
absolute = Pathname.new('/home/user/documents')
relative = Pathname.new('lib/utilities.rb')
current_dir = Pathname.new('.')
parent_dir = Pathname.new('..')

# Path properties
absolute.absolute?  # => true
relative.relative?  # => true
current_dir.directory?  # => true (if current directory exists)

Path joining represents the most common Pathname operation. The join method accepts multiple arguments and handles separator insertion automatically. This eliminates errors from manual string concatenation and separator management.

# Joining paths
base = Pathname.new('/usr/local')
bin_path = base.join('bin', 'ruby')
# => #<Pathname:/usr/local/bin/ruby>

# Complex joining
config_root = Pathname.new('config')
database_config = config_root.join('environments', 'production.yml')
cache_config = config_root.join('cache', 'redis.conf')

# Handles trailing separators correctly
path_with_slash = Pathname.new('/app/')
joined = path_with_slash.join('models', 'user.rb')
# => #<Pathname:/app/models/user.rb>

Path decomposition methods extract components from existing paths. These methods return Pathname objects for further manipulation or strings for direct use.

file_path = Pathname.new('/var/log/application.log.gz')

# Component extraction
file_path.dirname     # => #<Pathname:/var/log>
file_path.basename    # => #<Pathname:application.log.gz>
file_path.basename('.gz')  # => #<Pathname:application.log>
file_path.extname     # => ".gz"

# Path traversal
file_path.parent      # => #<Pathname:/var/log>
file_path.parent.parent  # => #<Pathname:/var>

File system queries determine path properties without loading file contents. These methods delegate to File and Dir classes but maintain the Pathname interface.

path = Pathname.new('config/database.yml')

# Existence checks
path.exist?           # => true/false
path.file?            # => true if regular file
path.directory?       # => true if directory
path.symlink?         # => true if symbolic link

# Permission checks
path.readable?        # => true if readable
path.writable?        # => true if writable
path.executable?      # => true if executable

# Size and timestamps
path.size             # => file size in bytes
path.mtime            # => modification time
path.ctime            # => creation time

Path resolution converts relative paths to absolute paths and resolves symbolic links. The expand_path method handles .. and . components and prepends the current working directory to relative paths.

# Path expansion
relative = Pathname.new('../config/app.yml')
absolute = relative.expand_path
# => #<Pathname:/home/user/project/config/app.yml>

# From specific directory
expanded = relative.expand_path('/opt/myapp')
# => #<Pathname:/opt/config/app.yml>

# Real path (resolves symlinks)
link_path = Pathname.new('current_version')  # symbolic link
real_path = link_path.realpath
# => #<Pathname:/opt/myapp/releases/20240315>

Reading and writing files through Pathname objects provides a convenient interface that handles path conversion internally. These methods delegate to File class methods but accept the same arguments.

config_path = Pathname.new('config/settings.json')

# Reading files
content = config_path.read
lines = config_path.readlines

# Writing files
config_path.write('{"debug": true}')
config_path.write('log entry', mode: 'a')  # append mode

# File operations
config_path.delete  # remove file
backup = Pathname.new('config/settings.json.bak')
config_path.rename(backup)  # move file

Error Handling & Debugging

File system operations through Pathname objects raise the same exceptions as their File and Dir counterparts. Understanding these exceptions helps build robust applications that handle missing files, permission issues, and other file system problems.

The most common exception is Errno::ENOENT, raised when attempting operations on non-existent files or directories. This occurs with read operations, file queries on missing files, and directory traversal through non-existent paths.

missing_file = Pathname.new('non_existent.txt')

begin
  content = missing_file.read
rescue Errno::ENOENT => e
  puts "File not found: #{e.message}"
  # Handle missing file - create default, prompt user, etc.
end

# Safer existence checking
if missing_file.exist?
  content = missing_file.read
else
  # Handle missing file case
  content = create_default_content
end

Permission errors manifest as Errno::EACCES exceptions when the process lacks required file system permissions. These errors occur during read, write, or execute operations on restricted files.

restricted_file = Pathname.new('/etc/passwd')

begin
  restricted_file.write('malicious content')
rescue Errno::EACCES => e
  puts "Permission denied: #{e.message}"
  # Log security attempt, notify admin, etc.
rescue Errno::ENOENT => e
  puts "File not found: #{e.message}"
end

# Check permissions before operations
if restricted_file.readable?
  content = restricted_file.read
else
  puts "Cannot read #{restricted_file}"
end

Directory operations introduce additional error conditions. Creating directories can fail due to parent directory absence, while directory removal fails when directories contain files.

deep_path = Pathname.new('/tmp/app/cache/sessions')

begin
  deep_path.mkdir
rescue Errno::ENOENT => e
  # Parent directory doesn't exist
  deep_path.parent.mkpath  # Create parent directories
  deep_path.mkdir          # Try again
rescue Errno::EEXIST => e
  # Directory already exists - usually safe to ignore
  puts "Directory exists: #{deep_path}"
end

# Safe directory creation
deep_path.mkpath  # Creates all parent directories if needed

Path resolution errors occur when symbolic links point to non-existent targets or when circular references exist. The realpath method raises exceptions for broken links, while expand_path handles most path normalization safely.

broken_link = Pathname.new('broken_symlink')

begin
  real_path = broken_link.realpath
rescue Errno::ENOENT => e
  puts "Broken symlink: #{e.message}"
  # Clean up broken link or recreate target
  broken_link.delete if broken_link.symlink?
end

# Check link status
if broken_link.symlink?
  begin
    target = broken_link.readlink
    puts "Link points to: #{target}"
  rescue Errno::ENOENT
    puts "Broken symlink detected"
  end
end

Debugging path issues often involves examining the actual path strings that Pathname objects contain. The to_s method reveals the internal string representation, while path decomposition methods help identify problematic components.

problematic_path = Pathname.new('//double//slashes/../weird/./path')

puts "Original: #{problematic_path.to_s}"
puts "Normalized: #{problematic_path.cleanpath.to_s}"
puts "Absolute: #{problematic_path.expand_path.to_s}"

# Component analysis
puts "Directory: #{problematic_path.dirname}"
puts "Basename: #{problematic_path.basename}"
puts "Parts: #{problematic_path.each_filename.to_a}"

File encoding issues can cause problems when reading files with non-ASCII content. Pathname delegates to File.read, which uses the default external encoding unless specified.

utf8_file = Pathname.new('unicode_content.txt')

begin
  content = utf8_file.read
rescue EncodingError => e
  puts "Encoding error: #{e.message}"
  # Try with explicit encoding
  content = utf8_file.read(encoding: 'utf-8')
end

# Specify encoding upfront
content = utf8_file.read(encoding: 'utf-8', invalid: :replace)

Production Patterns

Production applications require robust file handling patterns that account for concurrent access, error recovery, and system administration concerns. Pathname provides the foundation for these patterns while integrating with application frameworks and deployment tools.

Configuration file management represents a critical production pattern. Applications typically load configuration from multiple files with different precedence levels and environment-specific overrides.

class ConfigLoader
  def initialize(app_root)
    @app_root = Pathname.new(app_root)
    @config_dir = @app_root.join('config')
  end
  
  def load_config(environment = 'production')
    config = {}
    
    # Load base configuration
    base_config = @config_dir.join('application.yml')
    config.merge!(YAML.load_file(base_config)) if base_config.exist?
    
    # Load environment-specific config
    env_config = @config_dir.join("#{environment}.yml")
    config.merge!(YAML.load_file(env_config)) if env_config.exist?
    
    # Load local overrides (not in version control)
    local_config = @config_dir.join('local.yml')
    config.merge!(YAML.load_file(local_config)) if local_config.exist?
    
    config
  end
  
  def config_files_status
    [@config_dir.join('application.yml'),
     @config_dir.join('production.yml'),
     @config_dir.join('local.yml')].map do |path|
      {
        path: path.to_s,
        exists: path.exist?,
        readable: path.readable?,
        modified: path.exist? ? path.mtime : nil
      }
    end
  end
end

Log file management requires handling file rotation, directory creation, and permission management. Production applications often write to multiple log files with different retention policies.

class LogManager
  def initialize(log_root)
    @log_root = Pathname.new(log_root)
    ensure_log_directory
  end
  
  def application_log_path(date = Date.today)
    @log_root.join('application', "#{date.strftime('%Y-%m-%d')}.log")
  end
  
  def error_log_path
    @log_root.join('errors', 'error.log')
  end
  
  def rotate_logs(days_to_keep = 30)
    cutoff_date = Date.today - days_to_keep
    
    @log_root.join('application').children.select(&:file?).each do |log_file|
      if extract_date_from_filename(log_file) < cutoff_date
        log_file.delete
      end
    end
  end
  
  def ensure_log_permissions
    [@log_root.join('application'), @log_root.join('errors')].each do |dir|
      dir.chmod(0755) if dir.exist?
      dir.children.select(&:file?).each { |f| f.chmod(0644) }
    end
  end
  
  private
  
  def ensure_log_directory
    @log_root.mkpath unless @log_root.exist?
    [@log_root.join('application'), @log_root.join('errors')].each(&:mkpath)
  end
  
  def extract_date_from_filename(path)
    match = path.basename.to_s.match(/(\d{4}-\d{2}-\d{2})/)
    match ? Date.parse(match[1]) : Date.today
  end
end

Asset and static file serving in web applications requires path validation to prevent directory traversal attacks and efficient file serving with appropriate headers.

class StaticFileServer
  def initialize(public_root)
    @public_root = Pathname.new(public_root).expand_path
    @cache_control = 'public, max-age=31536000'  # 1 year
  end
  
  def serve_file(request_path)
    # Sanitize and resolve path
    clean_path = sanitize_path(request_path)
    file_path = @public_root.join(clean_path)
    
    # Security check - ensure file is within public root
    unless file_within_root?(file_path)
      raise SecurityError, "Path outside public directory: #{request_path}"
    end
    
    # File existence and type check
    return nil unless file_path.file?
    
    {
      path: file_path,
      content_type: determine_content_type(file_path),
      cache_control: cache_control_for_file(file_path),
      last_modified: file_path.mtime,
      size: file_path.size
    }
  end
  
  def precompile_assets(source_dir, target_dir)
    source_root = Pathname.new(source_dir)
    target_root = Pathname.new(target_dir)
    
    source_root.find do |source_file|
      next unless source_file.file?
      
      relative_path = source_file.relative_path_from(source_root)
      target_file = target_root.join(relative_path)
      
      target_file.parent.mkpath
      
      if needs_compilation?(source_file)
        compile_asset(source_file, target_file)
      else
        source_file.copy(target_file)
      end
    end
  end
  
  private
  
  def sanitize_path(path)
    # Remove query parameters and fragments
    clean = path.split('?').first.split('#').first
    
    # Decode URL encoding
    clean = URI.decode_www_form_component(clean)
    
    # Remove leading slash and resolve path
    Pathname.new(clean.sub(/^\//, '')).cleanpath.to_s
  end
  
  def file_within_root?(file_path)
    file_path.expand_path.to_s.start_with?(@public_root.to_s)
  end
  
  def determine_content_type(file_path)
    case file_path.extname.downcase
    when '.html' then 'text/html'
    when '.css' then 'text/css'
    when '.js' then 'application/javascript'
    when '.json' then 'application/json'
    when '.png' then 'image/png'
    when '.jpg', '.jpeg' then 'image/jpeg'
    else 'application/octet-stream'
    end
  end
  
  def cache_control_for_file(file_path)
    if file_path.to_s.include?('assets') && 
       file_path.basename.to_s.match?(/\w{8,}\.(css|js|png|jpg)$/)
      @cache_control  # Fingerprinted assets get long cache
    else
      'public, max-age=3600'  # Regular files get shorter cache
    end
  end
  
  def needs_compilation?(source_file)
    %w[.scss .sass .coffee .ts].include?(source_file.extname)
  end
  
  def compile_asset(source, target)
    # Asset compilation logic would go here
    # This is framework/tool specific
  end
end

Database backup and migration patterns often involve file system operations for backup storage and schema migration tracking.

class DatabaseBackupManager
  def initialize(backup_root, database_config)
    @backup_root = Pathname.new(backup_root)
    @database_config = database_config
    ensure_backup_structure
  end
  
  def create_backup(backup_name = nil)
    backup_name ||= "backup_#{Time.now.strftime('%Y%m%d_%H%M%S')}"
    backup_file = @backup_root.join('daily', "#{backup_name}.sql.gz")
    
    backup_file.parent.mkpath
    
    # Create database dump (simplified)
    dump_command = build_dump_command(backup_file)
    success = system(dump_command)
    
    if success
      create_backup_manifest(backup_file)
      cleanup_old_backups
      backup_file
    else
      backup_file.delete if backup_file.exist?
      raise "Backup failed: #{dump_command}"
    end
  end
  
  def restore_backup(backup_file_path)
    backup_file = Pathname.new(backup_file_path)
    
    unless backup_file.exist? && backup_file.readable?
      raise "Cannot read backup file: #{backup_file}"
    end
    
    restore_command = build_restore_command(backup_file)
    success = system(restore_command)
    
    unless success
      raise "Restore failed: #{restore_command}"
    end
    
    true
  end
  
  def list_backups
    @backup_root.join('daily').children
      .select { |f| f.file? && f.extname == '.gz' }
      .sort_by(&:mtime)
      .reverse
      .map { |f| backup_info(f) }
  end
  
  private
  
  def ensure_backup_structure
    %w[daily weekly monthly].each do |period|
      @backup_root.join(period).mkpath
    end
  end
  
  def build_dump_command(backup_file)
    "pg_dump #{@database_config['url']} | gzip > #{backup_file}"
  end
  
  def build_restore_command(backup_file)
    "gunzip -c #{backup_file} | psql #{@database_config['url']}"
  end
  
  def create_backup_manifest(backup_file)
    manifest_file = backup_file.sub_ext('.manifest.json')
    manifest = {
      backup_file: backup_file.basename.to_s,
      created_at: Time.now.iso8601,
      database: @database_config['database'],
      size_bytes: backup_file.size
    }
    manifest_file.write(JSON.pretty_generate(manifest))
  end
  
  def cleanup_old_backups
    cutoff_date = 30.days.ago
    @backup_root.join('daily').children.select(&:file?).each do |backup_file|
      if backup_file.mtime < cutoff_date
        backup_file.delete
        manifest_file = backup_file.sub_ext('.manifest.json')
        manifest_file.delete if manifest_file.exist?
      end
    end
  end
  
  def backup_info(backup_file)
    manifest_file = backup_file.sub_ext('.manifest.json')
    
    if manifest_file.exist?
      JSON.parse(manifest_file.read)
    else
      {
        backup_file: backup_file.basename.to_s,
        created_at: backup_file.mtime.iso8601,
        size_bytes: backup_file.size
      }
    end
  end
end

Reference

Core Methods

Method Parameters Returns Description
Pathname.new(path) path (String) Pathname Creates new Pathname object
#to_s None String Returns path as string
#to_path None String Alias for #to_s, used by File methods
#==(other) other (Object) Boolean Path equality comparison
#<=>(other) other (Pathname) Integer Path comparison for sorting
#hash None Integer Hash value for use as hash key

Path Information

Method Parameters Returns Description
#absolute? None Boolean True if path is absolute
#relative? None Boolean True if path is relative
#root? None Boolean True if path is root directory
#basename(suffix=nil) suffix (String, optional) Pathname Final path component
#dirname None Pathname Directory portion of path
#extname None String File extension including dot
#parent None Pathname Parent directory

Path Manipulation

Method Parameters Returns Description
#join(*paths) *paths (String/Pathname) Pathname Joins paths with separator
#+(path) path (String/Pathname) Pathname Alias for #join
#expand_path(base=nil) base (String, optional) Pathname Converts to absolute path
#realpath(base=nil) base (String, optional) Pathname Resolves symlinks to real path
#cleanpath None Pathname Removes redundant components
#relative_path_from(base) base (Pathname) Pathname Path relative to base directory

File System Queries

Method Parameters Returns Description
#exist? None Boolean True if path exists
#file? None Boolean True if regular file
#directory? None Boolean True if directory
#symlink? None Boolean True if symbolic link
#readable? None Boolean True if readable by process
#writable? None Boolean True if writable by process
#executable? None Boolean True if executable by process
#size None Integer File size in bytes
#zero? None Boolean True if file exists and is empty

File System Operations

Method Parameters Returns Description
#mkdir(mode=0777) mode (Integer) Integer Creates directory
#mkpath(mode=0777) mode (Integer) nil Creates directory and parents
#rmdir None Integer Removes empty directory
#rmtree None Integer Removes directory and contents
#delete None Integer Deletes file
#unlink None Integer Alias for #delete
#rename(new_name) new_name (String/Pathname) Integer Renames file or directory

File Content Operations

Method Parameters Returns Description
#read(*args) Variable args String Reads entire file content
#binread(*args) Variable args String Reads file in binary mode
#readlines(*args) Variable args Array<String> Reads file as array of lines
#write(content, *args) content (String), options Integer Writes content to file
#binwrite(content, *args) content (String), options Integer Writes binary content to file
#copy(dest) dest (String/Pathname) nil Copies file to destination

Directory Operations

Method Parameters Returns Description
#opendir(&block) Block (optional) Dir or result Opens directory for reading
#entries None Array<Pathname> Lists directory contents
#children(with_directory=true) with_directory (Boolean) Array<Pathname> Lists directory contents excluding . and ..
#each_child(&block) Block Enumerator Iterates over child paths
#find(&block) Block Enumerator Recursively finds all paths

Time and Permission Methods

Method Parameters Returns Description
#atime None Time Last access time
#mtime None Time Last modification time
#ctime None Time Creation time
#chmod(mode) mode (Integer) Integer Changes file permissions
#lchmod(mode) mode (Integer) Integer Changes link permissions
#chown(owner, group) owner, group (Integer) Integer Changes file ownership
#lchown(owner, group) owner, group (Integer) Integer Changes link ownership

Path Iteration

Method Parameters Returns Description
#each_filename(&block) Block Enumerator Iterates over path components
#ascend(&block) Block Enumerator Iterates from path to root
#descend(&block) Block Enumerator Iterates from root to path

Link Operations

Method Parameters Returns Description
#readlink None Pathname Reads symbolic link target
#make_link(target) target (String/Pathname) Integer Creates hard link
#make_symlink(target) target (String/Pathname) Integer Creates symbolic link

Constants and Exceptions

Constant Value Description
Pathname::SEPARATOR_PAT /[#{Regexp.quote(File::SEPARATOR)}#{Regexp.quote(File::ALT_SEPARATOR)}]/ Pattern matching path separators

Common Exceptions:

  • Errno::ENOENT - File or directory not found
  • Errno::EACCES - Permission denied
  • Errno::EEXIST - File or directory already exists
  • Errno::ENOTDIR - Not a directory
  • Errno::EISDIR - Is a directory (when file expected)
  • ArgumentError - Invalid path or arguments
  • SystemCallError - System call failures

File Mode Constants (for chmod operations):

  • 0644 - Read/write owner, read group/other
  • 0755 - Read/write/execute owner, read/execute group/other
  • 0666 - Read/write all users
  • 0777 - Read/write/execute all users