summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-01 23:09:35 +0200
committerPaul Buetow <paul@buetow.org>2026-03-01 23:09:35 +0200
commit98936da70412f87051e6237b882ce8f78d2431a2 (patch)
tree41270dd75d0c65a12aab1552fe0611f1e7998746
parent35f2d625367e6e476bcac7bf5b25a5cb4579fe92 (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>
-rw-r--r--lib/dsl.rb3
-rw-r--r--lib/dslkeywords/directory.rb104
-rw-r--r--lib/dslkeywords/file.rb207
-rw-r--r--lib/dslkeywords/file_backup.rb49
-rw-r--r--lib/dslkeywords/symlink.rb34
-rw-r--r--lib/dslkeywords/touch.rb36
6 files changed, 236 insertions, 197 deletions
diff --git a/lib/dsl.rb b/lib/dsl.rb
index 4a2c4ad..f4e34ac 100644
--- a/lib/dsl.rb
+++ b/lib/dsl.rb
@@ -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