Class: Tap::FileTask
- Includes:
- Support::ShellUtils
- Defined in:
- lib/tap/file_task.rb
Overview
FileTask provides methods for creating/modifying files such that you can rollback changes if an error occurs.
Creating Files/Rolling Back Changes
FileTask tracks which files to roll back using the added_files array and the backed_up_files hash. On an execute error, all added files are removed and then all backed up files (backed_up_files.keys) are restored
using the corresponding backup files (backed_up_files.values).
For consistency, all filepaths in added_files and backed_up_files should be expanded using File.expand_path. The easiest way to ensure files are properly set up for rollback is to use prepare before working with files and to create directories with mkdir.
# this file will be backed up and restored
File.open("file.txt", "w") {|f| f << "original content"}
t = FileTask.intern do |task|
task.mkdir("some/dir") # marked for rollback
task.prepare("file.txt", "path/to/file.txt") # marked for rollback
File.open("file.txt", "w") {|f| f << "new content"}
File.touch("path/to/file.txt")
# raise an error to start rollback
raise "error!"
end
begin
File.exists?("some/dir") # => false
File.exists?("path/to/file.txt") # => false
t.execute(nil)
rescue
$!. # => "error!"
File.exists?("some/dir") # => false
File.exists?("path/to/file.txt") # => false
File.read("file.txt") # => "original content"
end
Direct Known Subclasses
Constant Summary
Constants inherited from Task
Instance Attribute Summary collapse
-
#added_files ⇒ Object
readonly
An array of files added during task execution.
-
#backed_up_files ⇒ Object
readonly
A hash of backup [source, target] pairs, such that the backed-up files are backed_up_files.keys and the actual backup files are backed_up_files.values.
Attributes inherited from Task
Attributes included from Support::Executable
#_method_name, #app, #batch, #dependencies, #on_complete_block
Attributes included from Support::Configurable
Instance Method Summary collapse
-
#backup(list, backup_using_copy = false) ⇒ Object
Makes a backup of each file in list to backup_filepath(file) and registers the files so that they can be restored using restore.
-
#backup_filepath(filepath, time = Time.now) ⇒ Object
Makes a backup filepath relative to backup_dir by using self.name, the basename of filepath plus a timestamp.
-
#basename(path, extname = true) ⇒ Object
Returns the basename of path, exchanging the extension with extname.
-
#basepath(path, extname = false) ⇒ Object
Returns the path, exchanging the extension with extname.
-
#cleanup(pattern = /.*/) ⇒ Object
Removes backed-up files matching the pattern.
- #dir_empty?(dir) ⇒ Boolean
-
#filepath(dir, *paths) ⇒ Object
Constructs a filepath using the dir, name, and the specified paths.
-
#initialize(config = {}, name = nil, app = App.instance) ⇒ FileTask
constructor
A new instance of FileTask.
- #initialize_copy(orig) ⇒ Object
-
#log_basename(action, filepaths, level = Logger::INFO) ⇒ Object
Logs the given action, with the basenames of the input filepaths.
-
#mkdir(list) ⇒ Object
Creates the directories in list if they do not exist and adds them to added_files so they can be removed using rmdir.
-
#open(list, mode = "rb") ⇒ Object
A batch File.open method.
-
#prepare(list, backup_using_copy = false) ⇒ Object
Prepares the input list of files by backing them up (if they exist), ensuring that the parent directory for the file exists, and adding each file to added_files.
-
#restore(list) ⇒ Object
Restores each file in the input list using the backup file from backed_up_files.
-
#rm(list) ⇒ Object
Removes each file in the input list, provided the file is in added_files.
-
#rmdir(list) ⇒ Object
Removes each directory in the input list, provided the directory is in added_files and the directory is empty.
-
#rollback ⇒ Object
Rolls back changes by removing added_files and restoring backed_up_files.
-
#uptodate?(targets, sources = []) ⇒ Boolean
Returns true if all of the targets are up to date relative to all of the listed sources AND date_reference.
Methods included from Support::ShellUtils
Methods inherited from Task
execute, help, inherited, #initialize_batch_obj, #inspect, instance, intern, lazydoc, #log, parse, parse!, #process, #to_s
Methods included from Support::Executable
#_execute, #batch_index, #batch_with, #batched?, #check_terminate, #depends_on, #enq, #execute, #fork, initialize, #initialize_batch_obj, #inspect, #merge, #on_complete, #reset_dependencies, #resolve_dependencies, #sequence, #switch, #sync_merge, #unbatched_depends_on, #unbatched_enq, #unbatched_on_complete
Methods included from Support::Configurable
Constructor Details
#initialize(config = {}, name = nil, app = App.instance) ⇒ FileTask
Returns a new instance of FileTask.
69 70 71 72 73 74 |
# File 'lib/tap/file_task.rb', line 69 def initialize(config={}, name=nil, app=App.instance) super @backed_up_files = {} @added_files = [] end |
Instance Attribute Details
#added_files ⇒ Object
An array of files added during task execution.
56 57 58 |
# File 'lib/tap/file_task.rb', line 56 def added_files @added_files end |
#backed_up_files ⇒ Object
A hash of backup [source, target] pairs, such that the backed-up files are backed_up_files.keys and the actual backup files are backed_up_files.values. All filepaths in backed_up_files should be expanded.
53 54 55 |
# File 'lib/tap/file_task.rb', line 53 def backed_up_files @backed_up_files end |
Instance Method Details
#backup(list, backup_using_copy = false) ⇒ Object
Makes a backup of each file in list to backup_filepath(file) and registers the files so that they can be restored using restore. If backup_using_copy is true, the files will be copied to backup_filepath, otherwise the file is moved to backup_filepath. Raises an error if the file is already listed in backed_up_files.
Returns a list of the backup_filepaths.
file = "file.txt"
File.open(file, "w") {|f| f << "file content"}
t = FileTask.new
backed_up_file = t.backup(file).first
File.exists?(file) # => false
File.exists?(backed_up_file) # => true
File.read(backed_up_file) # => "file content"
File.open(file, "w") {|f| f << "new content"}
t.restore(file)
File.exists?(file) # => true
File.exists?(backed_up_file) # => false
File.read(file) # => "file content"
234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 |
# File 'lib/tap/file_task.rb', line 234 def backup(list, backup_using_copy=false) fu_list(list).collect do |filepath| next unless File.exists?(filepath) filepath = File.(filepath) if backed_up_files.include?(filepath) raise "Backup for #{filepath} already exists." end target = File.(backup_filepath(filepath)) dir = File.dirname(target) mkdir(dir) if backup_using_copy log :cp, "#{filepath} to #{target}", Logger::DEBUG FileUtils.cp(filepath, target) else log :mv, "#{filepath} to #{target}", Logger::DEBUG FileUtils.mv(filepath, target) end # track the target for restores backed_up_files[filepath] = target target end end |
#backup_filepath(filepath, time = Time.now) ⇒ Object
Makes a backup filepath relative to backup_dir by using self.name, the basename of filepath plus a timestamp.
t = FileTask.new({:timestamp => "%Y%m%d"}, 'name')
t.app['backup', true] = "/backup"
time = Time.utc(2008,8,8)
t.backup_filepath("path/to/file.txt", time) # => "/backup/name/file_20080808.txt"
179 180 181 182 183 |
# File 'lib/tap/file_task.rb', line 179 def backup_filepath(filepath, time=Time.now) extname = File.extname(filepath) backup_path = "#{File.basename(filepath).chomp(extname)}_#{time.strftime(timestamp)}#{extname}" filepath(backup_dir, backup_path) end |
#basename(path, extname = true) ⇒ Object
Returns the basename of path, exchanging the extension with extname. A false or nil extname removes the extension, while true preserves the existing extension.
t = FileTask.new
t.basename('path/to/file.txt') # => 'file.txt'
t.basename('path/to/file.txt', '.html') # => 'file.html'
t.basename('path/to/file.txt', false) # => 'file'
t.basename('path/to/file.txt', true) # => 'file.txt'
Compare to basepath.
155 156 157 |
# File 'lib/tap/file_task.rb', line 155 def basename(path, extname=true) basepath(File.basename(path), extname) end |
#basepath(path, extname = false) ⇒ Object
Returns the path, exchanging the extension with extname.
A false or nil extname removes the extension, while true preserves the existing extension (and effectively does nothing).
t = FileTask.new
t.basepath('path/to/file.txt') # => 'path/to/file'
t.basepath('path/to/file.txt', '.html') # => 'path/to/file.html'
t.basepath('path/to/file.txt', false) # => 'path/to/file'
t.basepath('path/to/file.txt', true) # => 'path/to/file.txt'
Compare to basename.
131 132 133 134 135 136 137 138 139 140 141 |
# File 'lib/tap/file_task.rb', line 131 def basepath(path, extname=false) case extname when false, nil path.chomp(File.extname(path)) when true path else extname = extname[1, extname.length-1] if extname[0] == ?. "#{path.chomp(File.extname(path))}.#{extname}" end end |
#cleanup(pattern = /.*/) ⇒ Object
Removes backed-up files matching the pattern.
514 515 516 517 518 519 520 521 522 523 524 |
# File 'lib/tap/file_task.rb', line 514 def cleanup(pattern=/.*/) backed_up_files.each do |filepath, target| next unless target =~ pattern # the filepath needs to be added to added_files # before it can be removed by rm added_files << target rm(target) backed_up_files.delete(filepath) end end |
#dir_empty?(dir) ⇒ Boolean
387 388 389 |
# File 'lib/tap/file_task.rb', line 387 def dir_empty?(dir) Dir.entries(dir).delete_if {|d| d == "." || d == ".."}.empty? end |
#filepath(dir, *paths) ⇒ Object
Constructs a filepath using the dir, name, and the specified paths.
t = FileTask.new
t.app[:data, true] = "/data"
t.name # => "tap/file_task"
t.filepath(:data, "result.txt") # => "/data/tap/file_task/result.txt"
166 167 168 |
# File 'lib/tap/file_task.rb', line 166 def filepath(dir, *paths) app.filepath(dir, name, *paths) end |
#initialize_copy(orig) ⇒ Object
76 77 78 79 80 |
# File 'lib/tap/file_task.rb', line 76 def initialize_copy(orig) super @backed_up_files = {} @added_files = [] end |
#log_basename(action, filepaths, level = Logger::INFO) ⇒ Object
Logs the given action, with the basenames of the input filepaths.
527 528 529 530 531 532 533 534 535 |
# File 'lib/tap/file_task.rb', line 527 def log_basename(action, filepaths, level=Logger::INFO) msg = case filepaths when Array then filepaths.collect {|filepath| File.basename(filepath) }.join(',') else File.basename(filepaths) end log(action, msg, level) end |
#mkdir(list) ⇒ Object
Creates the directories in list if they do not exist and adds them to added_files so they can be removed using rmdir. Creating directories in this way causes them to be rolled back upon an execution error.
Returns the made directories.
t = FileTask.new do |task, inputs|
File.exists?("path") # => false
task.mkdir("path/to/dir") # will be rolled back
File.exists?("path/to/dir") # => true
FileUtils.mkdir("path/to/another") # will not be rolled back
File.exists?("path/to/another") # => true
raise "error!"
end
begin
t.execute(nil)
rescue
$!. # => "error!"
File.exists?("path/to/dir") # => false
File.exists?("path/to/another") # => true
end
330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 |
# File 'lib/tap/file_task.rb', line 330 def mkdir(list) fu_list(list).each do |dir| dir = File.(dir) make_paths = [] while !File.exists?(dir) make_paths << dir dir = File.dirname(dir) end make_paths.reverse_each do |path| log :mkdir, path, Logger::DEBUG FileUtils.mkdir(path) added_files << path end end end |
#open(list, mode = "rb") ⇒ Object
A batch File.open method. If a block is given, each file in the list will be opened the open files passed to the block. Files are automatically closed when the block returns. If no block is given, the open files are returned.
t = FileTask.new
t.open(["one.txt", "two.txt"], "w") do |one, two|
one << "one"
two << "two"
end
File.read("one.txt") # => "one"
File.read("two.txt") # => "two"
Note that open normally takes and passes a list (ie an Array). If you provide a single argument, it will be translated into an Array, and passed AS AN ARRAY to the block.
t.open("file.txt", "w") do |array|
array.first << "content"
end
File.read("file.txt") # => "content"
105 106 107 108 109 110 111 112 113 114 115 116 |
# File 'lib/tap/file_task.rb', line 105 def open(list, mode="rb") open_files = [] begin [list].flatten.map {|path| path.to_str }.each do |filepath| open_files << File.open(filepath, mode) end block_given? ? yield(open_files) : open_files ensure open_files.each {|file| file.close } if block_given? end end |
#prepare(list, backup_using_copy = false) ⇒ Object
Prepares the input list of files by backing them up (if they exist), ensuring that the parent directory for the file exists, and adding each file to added_files. As a result the files can be removed using rm, restored using restore, and will be rolled back upon an execution error.
Returns the prepared files.
File.open("file.txt", "w") {|f| f << "original content"}
t = FileTask.new do |task, inputs|
File.exists?("path") # => false
# backup... make parent dirs... prepare for restore
task.prepare(["file.txt", "path/to/file.txt"])
File.open("file.txt", "w") {|f| f << "new content"}
File.touch("path/to/file.txt")
raise "error!"
end
begin
t.execute(nil)
rescue
$!. # => "error!"
File.exists?("file.txt") # => true
File.read("file.txt") # => "original content"
File.exists?("path") # => false
end
422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 |
# File 'lib/tap/file_task.rb', line 422 def prepare(list, backup_using_copy=false) list = fu_list(list) existing_files, non_existant_files = list.partition do |filepath| File.exists?(filepath) end # backup existing files existing_files.each do |filepath| backup(filepath, backup_using_copy) end # ensure the parent directory exists # for non-existant files non_existant_files.each do |filepath| dir = File.dirname(filepath) mkdir(dir) end list.each do |filepath| added_files << File.(filepath) end list end |
#restore(list) ⇒ Object
Restores each file in the input list using the backup file from backed_up_files. The backup directory is removed if it is empty.
Returns a list of the restored files.
file = "file.txt"
File.open(file, "w") {|f| f << "file content"}
t = FileTask.new
backed_up_file = t.backup(file).first
File.exists?(file) # => true
File.exists?(backed_up_file) # => true
File.read(backed_up_file) # => "file content"
File.open(file, "w") {|f| f << "new content"}
t.restore(file)
File.exists?(file) # => true
File.exists?(backed_up_file) # => false
File.read(file) # => "file content"
283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 |
# File 'lib/tap/file_task.rb', line 283 def restore(list) fu_list(list).collect do |filepath| filepath = File.(filepath) next unless backed_up_files.has_key?(filepath) target = backed_up_files.delete(filepath) dir = File.dirname(filepath) mkdir(dir) log :restore, "#{target} to #{filepath}", Logger::DEBUG FileUtils.mv(target, filepath, :force => true) dir = File.dirname(target) rmdir(dir) filepath end.compact end |
#rm(list) ⇒ Object
Removes each file in the input list, provided the file is in added_files.
The parent directory of each file is removed using rmdir. Removed files are removed from added_files.
Returns the removed files and directories.
t = FileTask.new
File.exists?("path") # => false
FileUtils.mkdir("path") # will not be removed
t.prepare("path/to/file.txt")
FileUtils.touch("path/to/file.txt")
File.exists?("path/to/file.txt") # => true
t.rm("path/to/file.txt")
File.exists?("path") # => true
File.exists?("path/to") # => false
464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 |
# File 'lib/tap/file_task.rb', line 464 def rm(list) removed = [] fu_list(list).each do |filepath| filepath = File.(filepath) next unless added_files.include?(filepath) # if the file exists, remove it if File.exists?(filepath) log :rm, filepath, Logger::DEBUG FileUtils.rm(filepath, :force => true) end removed << added_files.delete(filepath) removed.concat rmdir(File.dirname(filepath)) end removed end |
#rmdir(list) ⇒ Object
Removes each directory in the input list, provided the directory is in added_files and the directory is empty. When checking if the directory is empty, rmdir checks for regular files and hidden files. Removed directories are removed from added_files.
Returns a list of the removed directories.
t = FileTask.new
File.exists?("path") # => false
FileUtils.mkdir("path") # will not be removed
t.mkdir("path/to/dir")
File.exists?("path/to/dir") # => true
t.rmdir("path/to/dir")
File.exists?("path") # => true
File.exists?("path/to") # => false
365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 |
# File 'lib/tap/file_task.rb', line 365 def rmdir(list) removed = [] fu_list(list).each do |dir| dir = File.(dir) # remove directories and parents until the # directory was not made by the task while added_files.include?(dir) break unless dir_empty?(dir) if File.exists?(dir) log :rmdir, dir, Logger::DEBUG FileUtils.rmdir(dir) end removed << added_files.delete(dir) dir = File.dirname(dir) end end removed end |
#rollback ⇒ Object
Rolls back changes by removing added_files and restoring backed_up_files. Rollback is performed on an execute error if rollback_on_error == true, but is provided as a separate method for flexibility when needed. Yields errors to the block, which must be provided.
486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 |
# File 'lib/tap/file_task.rb', line 486 def rollback # :yields: error added_files.dup.each do |filepath| begin case when File.file?(filepath) rm(filepath) when File.directory?(filepath) rmdir(filepath) else # assures non-existant files are cleared from added_files # this is automatically done by rm and rmdir for existing files added_files.delete(filepath) end rescue yield $! end end backed_up_files.keys.each do |filepath| begin restore(filepath) rescue yield $! end end end |
#uptodate?(targets, sources = []) ⇒ Boolean
Returns true if all of the targets are up to date relative to all of the listed sources AND date_reference. Single values or arrays can be provided for both targets and sources.
Returns false (ie ‘not up to date’) if force?
is true.
191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 |
# File 'lib/tap/file_task.rb', line 191 def uptodate?(targets, sources=[]) if app.force log_basename(:force, *targets) false else targets = [targets] unless targets.kind_of?(Array) sources = [sources] unless sources.kind_of?(Array) # should be able to specify this somehow, externally set # sources << config_file unless config_file == nil targets.each do |target| return false unless FileUtils.uptodate?(target, sources) end true end end |