diff options
| author | Paul Buetow <paul@buetow.org> | 2026-03-01 23:09:35 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-03-01 23:09:35 +0200 |
| commit | 98936da70412f87051e6237b882ce8f78d2431a2 (patch) | |
| tree | 41270dd75d0c65a12aab1552fe0611f1e7998746 /lib | |
| parent | 35f2d625367e6e476bcac7bf5b25a5cb4579fe92 (diff) | |
refactor: split file.rb into per-class files under lib/dslkeywords/
file.rb was a ~400-line monolith holding seven unrelated classes/modules.
Extract each into its own file so each file has a single responsibility
and stays within the 50-line guideline:
file_backup.rb — FileBackup mixin
symlink.rb — Symlink class + DSL#symlink
touch.rb — Touch class + DSL#touch
directory.rb — Directory class + DSL#directory
file.rb keeps BasicFile, BaseFile, File, and DSL#file.
dsl.rb gains explicit require_relative lines for the new files.
No logic was changed; all 29 tests continue to pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/dsl.rb | 3 | ||||
| -rw-r--r-- | lib/dslkeywords/directory.rb | 104 | ||||
| -rw-r--r-- | lib/dslkeywords/file.rb | 207 | ||||
| -rw-r--r-- | lib/dslkeywords/file_backup.rb | 49 | ||||
| -rw-r--r-- | lib/dslkeywords/symlink.rb | 34 | ||||
| -rw-r--r-- | lib/dslkeywords/touch.rb | 36 |
6 files changed, 236 insertions, 197 deletions
@@ -4,6 +4,9 @@ require_relative 'log' require_relative 'chained' require_relative 'dslkeywords/file' +require_relative 'dslkeywords/symlink' +require_relative 'dslkeywords/touch' +require_relative 'dslkeywords/directory' require_relative 'dslkeywords/given' require_relative 'dslkeywords/notify' diff --git a/lib/dslkeywords/directory.rb b/lib/dslkeywords/directory.rb new file mode 100644 index 0000000..402bead --- /dev/null +++ b/lib/dslkeywords/directory.rb @@ -0,0 +1,104 @@ +require 'fileutils' + +require_relative 'file' + +module RCM + # Manages directories: create, delete/purge, or recursively copy from + # a source directory. Backup is performed before destructive operations. + class Directory < BaseFile + def recursively = @recursively = true + + def evaluate! + return unless super + + case @is + when :present + evaluate_present! + when :absent, :purged + evaluate_absent! + end + ensure + permissions! + end + + private + + def evaluate_present! + if ::File.directory?(@file_path) + return @recursively ? evaluate_present_recursively! : nil + end + + create_parent_directory! if @manage_directory + + do? "Creating directory #{@file_path}" do + Dir.mkdir(@file_path) + end + end + + def evaluate_absent! + return unless ::File.directory?(@file_path) + + backup!(@file_path) + @recursively = true if @is == :purged + what = @is == :purged ? 'Purging' : 'Deleting' + + do? "#{what} directory #{@file_path}" do + if ::File.directory?(@file_path) + @recursively ? FileUtils.rm_r(@file_path) : Dir.delete(@file_path) + end + end + cleanup_parent_directory! if @manage_directory + end + + def evaluate_present_recursively! + source_path = content + raise "Source #{source_path} is not a directory!" unless ::File.directory?(source_path) + + if ::File.exist?(@file_path) + raise "Destination #{@file_path} is not a directory!" unless ::File.directory?(@file_path) + + backup_recursively!(source_path, @file_path) unless @without_backup + end + + do? "Copying #{source_path} -> #{@file_path} recursively" do + if ::File.directory?(@file_path) + Dir["#{source_path}/*"].each { FileUtils.cp_r(_1, @file_path) } + else + FileUtils.cp_r(source_path, @file_path) + end + end + end + + # TODO: Unit test this + def backup_recursively!(source, dest) + Dir.foreach(source) do |entry| + next if ['.', '..'].include?(entry) + + source_path = ::File.join(source, entry) + dest_path = ::File.join(dest, entry) + + if ::File.directory?(source_path) && !::File.directory?(dest_path) + raise "Unable to copy directory #{source_path} into non-directory #{dest_path}" + elsif !::File.directory?(source_path) && ::File.directory?(dest_path) + raise "Unable to copy non-directory #{source_path} into directory #{dest_path}" + elsif ::File.directory?(source_path) && ::File.directory?(dest_path) + backup_recursively!(source_path, dest_path) + else + backup!(dest_path) + end + end + end + end + + class DSL + def directory(file_path = nil, &block) + return :directory if file_path.nil? + return unless @conds_met + + d = Directory.new(file_path) + d.content(d.instance_eval(&block)) + self << d + d + end + end +end diff --git a/lib/dslkeywords/file.rb b/lib/dslkeywords/file.rb index bf31e6a..273968c 100644 --- a/lib/dslkeywords/file.rb +++ b/lib/dslkeywords/file.rb @@ -4,53 +4,13 @@ require 'fileutils' require_relative 'resource' require_relative '../chained' +require_relative 'file_backup' module RCM - # Backup the file on change - module FileBackup - # TODO: Make protected? - def backup!(file_path, checksum = nil) - return if @without_backup - - suffix = if ::File.file?(file_path) - checksum.nil? ? Digest::SHA256.file(file_path).hexdigest : checksum - else - Time.now.strftime('%s-%L') - end - make_backup!(file_path, suffix) - end - - def different?(file_a, file_b) - checksum_a = Digest::SHA256.file(file_a).hexdigest - checksum_b = Digest::SHA256.file(file_b).hexdigest - [checksum_a != checksum_b, checksum_a, checksum_b] - end - - private - - def make_backup!(file_path, suffix) - backup_dir = create_backup_directory!(file_path) - backup_path = "#{backup_dir}/#{::File.basename(file_path)}.#{suffix}" - return if ::File.exist?(backup_path) - - do? "Backing up #{file_path} -> #{backup_path}" do - ::File.rename(file_path, backup_path) - end - end - - def create_backup_directory!(file_path) - backup_dir = "#{::File.dirname(file_path)}/.rcmbackup" - return backup_dir if ::File.directory?(backup_dir) - - do? "Creating backup directory #{backup_dir}" do - Dir.mkdir(backup_dir) - end - - backup_dir - end - end - - # Base for BaseFile and Directory + # Base class shared by all file-system resources (files, symlinks, + # touch, directories). Manages path, state (:present/:absent/:purged), + # permissions (mode/owner/group), parent-directory lifecycle, and the + # FileBackup mixin. class BasicFile < Resource include Chained include FileBackup @@ -95,7 +55,7 @@ module RCM set_owner!(stat) end - # Validate whether we can use this up in this context or not + # Reject DSL options that are not valid for this resource type. def validate(method, what, *valids) return what if valids.include?(what) @@ -149,7 +109,8 @@ module RCM end end - # Base for File and Symlink + # Intermediate base for resources that have file content and support + # :sourcefile / :template sourcing, and absent-state deletion. class BaseFile < BasicFile class UnsupportedOperation < StandardError; end @@ -168,7 +129,8 @@ module RCM end end - # Managing files + # Manages regular files: write content, ensure/remove individual lines, + # delete. Writes via a temp file so the final rename is atomic. class File < BaseFile def line(line) = @ensure_line = line @@ -237,125 +199,6 @@ module RCM end end - # Manage symlinks - class Symlink < BaseFile - def evaluate! - return unless super - return evaluate_absent! if %i[absent purged].include?(@is) - return if ::File.symlink?(@file_path) && ::File.readlink(@file_path) == content - - create_parent_directory! if @manage_directory - do? "Creating symlink #{@file_path}" do - FileUtils.ln_sf(content, @file_path) - end - ensure - permissions! - end - end - - # Emtpy file - class Touch < BaseFile - def is(what) = @is = validate(__method__, what.to_sym, :present, :absent, :purged, :updated) - - def evaluate! - return unless super - return evaluate_absent! if %i[absent purged].include?(@is) - return if ::File.file?(@file_path) && @is != :updated - - create_parent_directory! if @manage_directory - do? "Touching #{@file_path}" do - FileUtils.touch(@file_path) - end - ensure - permissions! - end - end - - class Directory < BaseFile - def recursively = @recursively = true - - def evaluate! - return unless super - - case @is - when :present - evaluate_present! - when :absent, :purged - evaluate_absent! - end - ensure - permissions! - end - - private - - def evaluate_present! - if ::File.directory?(@file_path) - return @recursively ? evaluate_present_recursively! : nil - end - - create_parent_directory! if @manage_directory - - do? "Creating directory #{@file_path}" do - Dir.mkdir(@file_path) - end - end - - def evaluate_absent! - return unless ::File.directory?(@file_path) - - backup!(@file_path) - @recursively = true if @is == :purged - what = @is == :purged ? 'Purging' : 'Deleting' - - do? "#{what} directory #{@file_path}" do - if ::File.directory?(@file_path) - @recursively ? FileUtils.rm_r(@file_path) : Dir.delete(@file_path) - end - end - cleanup_parent_directory! if @manage_directory - end - - def evaluate_present_recursively! - source_path = content - raise "Source #{source_path} is not a directory!" unless ::File.directory?(source_path) - - if ::File.exist?(@file_path) - raise "Destination #{@file_path} is not a directory!" unless ::File.directory?(@file_path) - - backup_recursively!(source_path, @file_path) unless @without_backup - end - - do? "Copying #{source_path} -> #{@file_path} recursively" do - if ::File.directory?(@file_path) - Dir["#{source_path}/*"].each { FileUtils.cp_r(_1, @file_path) } - else - FileUtils.cp_r(source_path, @file_path) - end - end - end - - # TODO: Unit test this - def backup_recursively!(source, dest) - Dir.foreach(source) do |entry| - next if ['.', '..'].include?(entry) - - source_path = ::File.join(source, entry) - dest_path = ::File.join(dest, entry) - - if ::File.directory?(source_path) && !::File.directory?(dest_path) - raise "Unable to copy directory #{source_path} into non-directory #{dest_path}" - elsif !::File.directory?(source_path) && ::File.directory?(dest_path) - raise "Unable to copy non-directory #{source_path} into directory #{dest_path}" - elsif ::File.directory?(source_path) && ::File.directory?(dest_path) - backup_recursively!(source_path, dest_path) - else - backup!(dest_path) - end - end - end - end - class DSL # Add file keyword to the DSL def file(file_path = nil, &block) @@ -367,35 +210,5 @@ module RCM self << f f end - - def symlink(file_path = nil, &block) - return :symlink if file_path.nil? - return unless @conds_met - - s = Symlink.new(file_path) - s.content(s.instance_eval(&block)) - self << s - s - end - - def touch(file_path = nil, &block) - return :touch if file_path.nil? - return unless @conds_met - - t = Touch.new(file_path) - t.instance_eval(&block) if block - self << t - t - end - - def directory(file_path = nil, &block) - return :directory if file_path.nil? - return unless @conds_met - - d = Directory.new(file_path) - d.content(d.instance_eval(&block)) - self << d - d - end end end diff --git a/lib/dslkeywords/file_backup.rb b/lib/dslkeywords/file_backup.rb new file mode 100644 index 0000000..210804c --- /dev/null +++ b/lib/dslkeywords/file_backup.rb @@ -0,0 +1,49 @@ +require 'digest' + +module RCM + # Mixin that provides file-backup helpers for resource classes. + # Included by BasicFile so all file/directory/symlink resources share + # the same backup logic. + module FileBackup + # TODO: Make protected? + def backup!(file_path, checksum = nil) + return if @without_backup + + suffix = if ::File.file?(file_path) + checksum.nil? ? Digest::SHA256.file(file_path).hexdigest : checksum + else + Time.now.strftime('%s-%L') + end + make_backup!(file_path, suffix) + end + + def different?(file_a, file_b) + checksum_a = Digest::SHA256.file(file_a).hexdigest + checksum_b = Digest::SHA256.file(file_b).hexdigest + [checksum_a != checksum_b, checksum_a, checksum_b] + end + + private + + def make_backup!(file_path, suffix) + backup_dir = create_backup_directory!(file_path) + backup_path = "#{backup_dir}/#{::File.basename(file_path)}.#{suffix}" + return if ::File.exist?(backup_path) + + do? "Backing up #{file_path} -> #{backup_path}" do + ::File.rename(file_path, backup_path) + end + end + + def create_backup_directory!(file_path) + backup_dir = "#{::File.dirname(file_path)}/.rcmbackup" + return backup_dir if ::File.directory?(backup_dir) + + do? "Creating backup directory #{backup_dir}" do + Dir.mkdir(backup_dir) + end + + backup_dir + end + end +end diff --git a/lib/dslkeywords/symlink.rb b/lib/dslkeywords/symlink.rb new file mode 100644 index 0000000..160da6f --- /dev/null +++ b/lib/dslkeywords/symlink.rb @@ -0,0 +1,34 @@ +require 'fileutils' + +require_relative 'file' + +module RCM + # Manages symbolic links: creates or removes them, optionally under + # a managed parent directory, and applies permissions afterwards. + class Symlink < BaseFile + def evaluate! + return unless super + return evaluate_absent! if %i[absent purged].include?(@is) + return if ::File.symlink?(@file_path) && ::File.readlink(@file_path) == content + + create_parent_directory! if @manage_directory + do? "Creating symlink #{@file_path}" do + FileUtils.ln_sf(content, @file_path) + end + ensure + permissions! + end + end + + class DSL + def symlink(file_path = nil, &block) + return :symlink if file_path.nil? + return unless @conds_met + + s = Symlink.new(file_path) + s.content(s.instance_eval(&block)) + self << s + s + end + end +end diff --git a/lib/dslkeywords/touch.rb b/lib/dslkeywords/touch.rb new file mode 100644 index 0000000..d1073f2 --- /dev/null +++ b/lib/dslkeywords/touch.rb @@ -0,0 +1,36 @@ +require 'fileutils' + +require_relative 'file' + +module RCM + # Creates an empty file (touch semantics). Supports the additional + # :updated state which re-touches the file even when it already exists. + class Touch < BaseFile + def is(what) = @is = validate(__method__, what.to_sym, :present, :absent, :purged, :updated) + + def evaluate! + return unless super + return evaluate_absent! if %i[absent purged].include?(@is) + return if ::File.file?(@file_path) && @is != :updated + + create_parent_directory! if @manage_directory + do? "Touching #{@file_path}" do + FileUtils.touch(@file_path) + end + ensure + permissions! + end + end + + class DSL + def touch(file_path = nil, &block) + return :touch if file_path.nil? + return unless @conds_met + + t = Touch.new(file_path) + t.instance_eval(&block) if block + self << t + t + end + end +end |
