Class: Tap::FileTask

Inherits:
Task show all
Includes:
Support::ShellUtils
Defined in:
lib/tap/file_task.rb

Overview

FileTask is a base class for tasks that work with a file system. FileTask tracks changes it makes so they may be rolled back to their original state. Rollback automatically occurs on an execute error.

File.open("file.txt", "w") {|file| file << "original content"}

t = FileTask.intern do |task, raise_error|
  task.mkdir_p("some/dir")              # marked for rollback
  task.prepare("file.txt") do |file|    # marked for rollback
    file << "new content"
  end

  # raise an error to start rollback
  raise "error!" if raise_error
end

begin
  t.execute(true)
rescue
  $!.message                            # => "error!"
  File.exists?("some/dir")              # => false
  File.read("file.txt")                 # => "original content"
end

t.execute(false)
File.exists?("some/dir")                # => true
File.read("file.txt")                   # => "new content"

Direct Known Subclasses

Tasks::Dump

Constant Summary

Constants inherited from Task

Task::DEFAULT_HELP_TEMPLATE

Instance Attribute Summary

Attributes inherited from Task

#name

Attributes included from Support::Executable

#app, #batch, #dependencies, #method_name, #on_complete_block

Instance Method Summary collapse

Methods included from Support::ShellUtils

#capture_sh, #redirect_sh, #sh

Methods inherited from Task

execute, help, inherited, #initialize_batch_obj, #inspect, instance, intern, load, #log, parse, parse!, #process, #to_s, use

Methods included from Support::Executable

#_execute, #batch_index, #batch_with, #batched?, #check_terminate, #depends_on, #enq, #execute, #fork, initialize, #initialize_batch_obj, #merge, #on_complete, #reset_dependencies, #resolve_dependencies, #sequence, #switch, #sync_merge, #unbatched_depends_on, #unbatched_enq, #unbatched_on_complete

Constructor Details

#initialize(config = {}, name = nil, app = App.instance) ⇒ FileTask

Returns a new instance of FileTask.



44
45
46
47
# File 'lib/tap/file_task.rb', line 44

def initialize(config={}, name=nil, app=App.instance)
  super
  @actions = []
end

Instance Method Details

#backup(path, backup_using_copy = false) ⇒ Object

Makes a backup of path to backup_filepath(path) and returns the backup path. If backup_using_copy is true, the backup is a copy of path, otherwise the file or directory at path is moved to the backup path. Raises an error if the backup file already exists.

Backups are restored on rollback.

file = "file.txt"
File.open(file, "w") {|f| f << "file content"}

t = FileTask.new
backup_file = t.backup(file)

File.exists?(file)                       # => false
File.exists?(backup_file)                # => true
File.read(backup_file)                   # => "file content"

File.open(file, "w") {|f| f << "new content"}
t.rollback

File.exists?(file)                       # => true
File.exists?(backup_file   )             # => false
File.read(file)                          # => "file content"


162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/tap/file_task.rb', line 162

def backup(path, backup_using_copy=false)
  return nil unless File.exists?(path)
    
  source = File.expand_path(path)
  target = backup_filepath(source)
  raise "backup file already exists: #{target}" if File.exists?(target)
  
  mkdir_p File.dirname(target)
  
  log :backup, "#{source} to #{target}", Logger::DEBUG
  if backup_using_copy
    FileUtils.cp(source, target)
  else
    FileUtils.mv(source, target)
  end
  
  actions << [:backup, source, target]
  target
end

#backup_filepath(path) ⇒ Object

Makes a backup filepath relative to backup_dir by using name, the basename of filepath, and an index.

t = FileTask.new({:backup_dir => "/backup"}, "name")
t.backup_filepath("path/to/file.txt", time)     # => "/backup/name/file.0.txt"


109
110
111
112
113
# File 'lib/tap/file_task.rb', line 109

def backup_filepath(path)
  extname = File.extname(path)
  backup_path = filepath(backup_dir, File.basename(path).chomp(extname))
  next_indexed_path(backup_path, 0, extname)
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.



88
89
90
# File 'lib/tap/file_task.rb', line 88

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.



68
69
70
71
72
73
74
# File 'lib/tap/file_task.rb', line 68

def basepath(path, extname=false)
  case extname
  when false, nil then path.chomp(File.extname(path))
  when true then path
  else Root.exchange(path, extname)
  end
end

#cleanup(cleanup_dirs = true) ⇒ Object

Removes backup files. Cleanup cannot be rolled back and prevents rollback of actions up to when cleanup is called. If cleanup_dirs is true, empty directories containing the backup files will be removed.



331
332
333
334
335
336
337
338
339
340
# File 'lib/tap/file_task.rb', line 331

def cleanup(cleanup_dirs=true)
  actions.each do |action, source, target|
    if action == :backup
      log :cleanup, target, Logger::DEBUG
      FileUtils.rm_r(target) if File.exists?(target)
      cleanup_dir(File.dirname(target)) if cleanup_dirs
    end
  end
  actions.clear
end

#cleanup_dir(dir) ⇒ Object

Removes the directory if empty, and all empty parent directories. This method cannot be rolled back.



344
345
346
347
348
349
350
# File 'lib/tap/file_task.rb', line 344

def cleanup_dir(dir)
  while Root.empty?(dir)
    log :rmdir, dir, Logger::DEBUG
    FileUtils.rmdir(dir)
    dir = File.dirname(dir)
  end
end

#cp(source, target) ⇒ Object

Copies source to target. Files and directories copied by cp are restored upon an execution error.



273
274
275
276
277
278
279
# File 'lib/tap/file_task.rb', line 273

def cp(source, target)
  target = File.join(target, File.basename(source)) if File.directory?(target)
  prepare(target)
  
  log :cp, "#{source} to #{target}", Logger::DEBUG
  FileUtils.cp(source, target)
end

#cp_r(source, target) ⇒ Object

Copies source to target. If source is a directory, the contents are copied recursively. If target is a directory, copies source to target/source. Files and directories copied by cp are restored upon an execution error.



285
286
287
288
289
290
291
# File 'lib/tap/file_task.rb', line 285

def cp_r(source, target)
  target = File.join(target, File.basename(source)) if File.directory?(target)
  prepare(target)
  
  log :cp_r, "#{source} to #{target}", Logger::DEBUG
  FileUtils.cp_r(source, target)
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"


99
100
101
# File 'lib/tap/file_task.rb', line 99

def filepath(dir, *paths) 
  app.filepath(dir, name, *paths)
end

#initialize_copy(orig) ⇒ Object

Initializes a copy that will rollback independent of self.



50
51
52
53
# File 'lib/tap/file_task.rb', line 50

def initialize_copy(orig)
  super
  @actions = []
end

#log_basename(action, paths, level = Logger::INFO) ⇒ Object

Logs the given action, with the basenames of the input paths.



353
354
355
356
# File 'lib/tap/file_task.rb', line 353

def log_basename(action, paths, level=Logger::INFO)
  msg = [paths].flatten.collect {|path| File.basename(path) }.join(',')
  log(action, msg, level)
end

#mkdir(dir) ⇒ Object

Creates a directory. Directories created by mkdir removed on rollback.



197
198
199
200
201
202
203
204
205
# File 'lib/tap/file_task.rb', line 197

def mkdir(dir)
  dir = File.expand_path(dir)
  
  unless File.exists?(dir)
    log :mkdir, dir, Logger::DEBUG
    FileUtils.mkdir(dir)
    actions << [:make, dir]
  end
end

#mkdir_p(dir) ⇒ Object

Creates a directory and all its parent directories. Directories created by mkdir_p removed on rollback.



184
185
186
187
188
189
190
191
192
193
194
# File 'lib/tap/file_task.rb', line 184

def mkdir_p(dir)
  dir = File.expand_path(dir)
    
  dirs = []
  while !File.exists?(dir)
    dirs.unshift(dir)
    dir = File.dirname(dir)
  end
    
  dirs.each {|dir| mkdir(dir) }
end

#mv(source, target, backup_source = true) ⇒ Object

Moves source to target. Files and directories moved by mv are restored upon an execution error.



295
296
297
298
299
300
301
# File 'lib/tap/file_task.rb', line 295

def mv(source, target, backup_source=true)
  backup(source, true) if backup_source
  prepare(target)
  
  log :mv, "#{source} to #{target}", Logger::DEBUG
  FileUtils.mv(source, target)
end

#prepare(path, backup_using_copy = false) ⇒ Object

Prepares the path by backing up any existing file and ensuring that the parent directory for path exists. If a block is given, a file is opened and yielded to it (as in File.open). Prepared paths are removed and the backups restored on rollback.

Returns the expanded path.



213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
# File 'lib/tap/file_task.rb', line 213

def prepare(path, backup_using_copy=false) 
  raise "not a file: #{path}" if File.directory?(path)
  path = File.expand_path(path)

  if File.exists?(path)
   # backup or remove existing files
    backup(path, backup_using_copy)
  else
    # ensure the parent directory exists
    # for non-existant files 
    mkdir_p File.dirname(path)
  end
  log :prepare, path, Logger::DEBUG
  actions << [:make, path]
  
  if block_given?
    File.open(path, "w") {|file| yield(file) }
  end
  
  path
end

#rm(path) ⇒ Object

Removes a file. Directories cannot be removed by this method. Files removed by rm are restored upon an execution error.



260
261
262
263
264
265
266
267
268
269
# File 'lib/tap/file_task.rb', line 260

def rm(path) 
  path = File.expand_path(path)
  
  unless File.file?(path)
    raise "not a file: #{path}"
  end
  
  backup(path, false)
  log :rm, path, Logger::DEBUG
end

#rm_r(path) ⇒ Object

Removes a file. If a directory is provided, it’s contents are removed recursively. Files and directories removed by rm_r are restored upon an execution error.



238
239
240
241
242
243
# File 'lib/tap/file_task.rb', line 238

def rm_r(path) 
  path = File.expand_path(path)
  
  backup(path, false)
  log :rm_r, path, Logger::DEBUG
end

#rmdir(dir) ⇒ Object

Removes an empty directory. Directories removed by rmdir are restored upon an execution error.



247
248
249
250
251
252
253
254
255
256
# File 'lib/tap/file_task.rb', line 247

def rmdir(dir)
  dir = File.expand_path(dir)
  
  unless Root.empty?(dir)
    raise "not an empty directory: #{dir}"
  end
  
  backup(dir, false)
  log :rmdir, dir, Logger::DEBUG
end

#rollbackObject

Rolls back any actions capable of being rolled back. Rollback is forceful; for instance if you make a folder using mkdir rollback removes that directory using FileUtils.rm_r. Any files added to the folder will be removed even if they were not added by self.



308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
# File 'lib/tap/file_task.rb', line 308

def rollback
  while !actions.empty?
    action, source, target = actions.pop

    case action
    when :make
      log :rollback, "#{source}", Logger::DEBUG
      FileUtils.rm_r(source)
    when :backup
      log :rollback, "#{target} to #{source}", Logger::DEBUG
      dir = File.dirname(source)
      FileUtils.mkdir_p(dir) unless File.exists?(dir)
      FileUtils.mv(target, source, :force => true)
    else
      raise "unknown action: #{[action, source, target].inspect}"
    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. Single values or arrays can be provided for both targets and sources.

Returns false (ie ‘not up to date’) if app.force is true.

– TODO: add check vs date reference (ex config_file date)

Returns:

  • (Boolean)


123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/tap/file_task.rb', line 123

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)
    
    targets.each do |target|
      return false unless FileUtils.uptodate?(target, sources)
    end 
    true
  end
end