Class: Tap::Tasks::FileTask

Inherits:
Tap::Task
  • Object
show all
Includes:
ShellUtils
Defined in:
lib/tap/tasks/file_task.rb,
lib/tap/tasks/file_task/shell_utils.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"

Defined Under Namespace

Modules: ShellUtils

Instance Method Summary collapse

Methods included from ShellUtils

#capture_sh, #redirect_sh, #sh

Constructor Details

#initialize(config = {}, app = Tap::App.instance) ⇒ FileTask

Returns a new instance of FileTask.



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

def initialize(config={}, app=Tap::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 path 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"


159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# File 'lib/tap/tasks/file_task.rb', line 159

def backup(path, backup_using_copy=false)
  return nil unless File.exists?(path)
  
  source = File.expand_path(path)
  target = backup_filepath(source)
  raise "backup 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"


106
107
108
109
110
# File 'lib/tap/tasks/file_task.rb', line 106

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.



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

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.



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

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

#call(*_inputs) ⇒ Object



355
356
357
358
359
360
361
362
363
364
# File 'lib/tap/tasks/file_task.rb', line 355

def call(*_inputs)
  actions.clear

  begin
    super
  rescue(Exception)
    rollback if rollback_on_error
    raise
  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.



328
329
330
331
332
333
334
335
336
337
# File 'lib/tap/tasks/file_task.rb', line 328

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.



341
342
343
344
345
346
347
# File 'lib/tap/tasks/file_task.rb', line 341

def cleanup_dir(dir)
  while Root::Utils.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.



270
271
272
273
274
275
276
# File 'lib/tap/tasks/file_task.rb', line 270

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.



282
283
284
285
286
287
288
# File 'lib/tap/tasks/file_task.rb', line 282

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 and the specified paths.

t = FileTask.new
t.filepath('data', "result.txt")      # => File.expand_path("data/tap/tasks/file_task/result.txt")


96
97
98
# File 'lib/tap/tasks/file_task.rb', line 96

def filepath(dir, *paths)
  File.expand_path(File.join(dir, *paths))
end

#initialize_copy(orig) ⇒ Object

Initializes a copy that will rollback independent of self.



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

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.



350
351
352
353
# File 'lib/tap/tasks/file_task.rb', line 350

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.



194
195
196
197
198
199
200
201
202
# File 'lib/tap/tasks/file_task.rb', line 194

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.



181
182
183
184
185
186
187
188
189
190
191
# File 'lib/tap/tasks/file_task.rb', line 181

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

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

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



292
293
294
295
296
297
298
# File 'lib/tap/tasks/file_task.rb', line 292

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.



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

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.



257
258
259
260
261
262
263
264
265
266
# File 'lib/tap/tasks/file_task.rb', line 257

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.



235
236
237
238
239
240
# File 'lib/tap/tasks/file_task.rb', line 235

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.



244
245
246
247
248
249
250
251
252
253
# File 'lib/tap/tasks/file_task.rb', line 244

def rmdir(dir)
  dir = File.expand_path(dir)

  unless Root::Utils.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 will remove the folder and all files within it even if they were not added by self.



305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
# File 'lib/tap/tasks/file_task.rb', line 305

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)


120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/tap/tasks/file_task.rb', line 120

def uptodate?(targets, sources=[])
  if app && 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